import React from "react"; import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; import { z } from "zod"; import { colorSchema } from "../lib/branding"; import { FONT } from "../lib/fonts"; import { useLayout } from "../lib/aspect"; import { BrandBackground, useReveal } from "../lib/kit"; import { hexToRgba } from "../lib/anim"; export const countdownSchema = z.object({ title: z.string(), // coerce so a string binding ("5") from the studio still validates as a number startNumber: z.coerce.number().int().min(1).max(9), goText: z.string(), subtitle: z.string(), ...colorSchema, }); type Props = z.infer; export const Countdown: React.FC = ({ title, startNumber, goText, subtitle, accentColor, secondaryColor, backgroundColor, textColor, }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const L = useLayout(); const titleR = useReveal(6, { from: 24 }); // Count down one number per second after a short intro. const introF = Math.round(fps * 1.2); const elapsed = Math.max(0, frame - introF); const sec = Math.floor(elapsed / fps); const current = startNumber - sec; // >0 → number, <=0 → GO const localInSec = (elapsed % fps) / fps; // Each tick pops in and fades/scales out. const pop = spring({ frame: (elapsed % fps), fps, config: { damping: 12, stiffness: 130, mass: 0.7 } }); const scaleIn = interpolate(pop, [0, 1], [0.4, 1]); const scaleOut = interpolate(localInSec, [0.7, 1], [1, 1.4], { extrapolateLeft: "clamp" }); const fadeOut = interpolate(localInSec, [0.75, 1], [1, 0], { extrapolateLeft: "clamp" }); const isGo = current <= 0; const ringProgress = 1 - localInSec; const ringR = L.vmin(220); const circ = 2 * Math.PI * ringR; const sub = useReveal(introF + 4, { from: 24 }); return (
{title}
{/* Progress ring */} {!isGo && ( )}
{isGo ? goText : current}
{subtitle}
); };