{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "countdown",
  "title": "Countdown",
  "description": "A monumental countdown numeral inside a ring resolves into a Live lockup.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/countdown/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/BricolageGrotesque\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport type React from \"react\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: sans } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"600\"],\n  subsets: [\"latin\"],\n});\n\nconst REMOCN_R_PATH =\n  \"M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z\";\n\n/** Full logo as on remocn.dev: R mark (0.76em tall, baseline-aligned) + \"emocn\" in Manrope 600. */\nfunction RemocnLogo({ fontSize, color }: { fontSize: number; color: string }) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"baseline\",\n        color,\n        fontFamily: manrope.fontFamily,\n        fontWeight: 600,\n        fontSize,\n        letterSpacing: \"-0.025em\",\n        lineHeight: 1,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 124.06 134.26\"\n        style={{ height: \"0.76em\", width: \"auto\" }}\n        aria-hidden\n      >\n        <path fill=\"currentColor\" d={REMOCN_R_PATH} />\n      </svg>\n      <span style={{ marginLeft: \"0.04em\" }}>emocn</span>\n    </div>\n  );\n}\n\nexport interface CountdownProps {\n  /** First number of the countdown. */\n  from?: number;\n  /** What the countdown lands on: \"Live\", a product name, a date. */\n  finalLabel?: string;\n  /** Show the small accent dot before the final label. */\n  indicator?: boolean;\n  /** Small lockup under the final label. Pass \"\" to hide it. */\n  caption?: string;\n  /** Replaces the default remocn mark in the lockup. */\n  logo?: React.ReactNode;\n  /** Frames per number. */\n  beat?: number;\n  accent?: string;\n  /** Playback multiplier. */\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    fg: \"#F3F2EF\",\n    muted: \"#8B8A90\",\n    track: \"rgba(255,255,255,0.10)\",\n    stage: \"rgba(255,255,255,0.06)\",\n    push: \"rgba(255,255,255,0.09)\",\n    grain: 0.06,\n    /** gradients.sh \"radial-glow\" (#f3f1ff #9db4ff #5a46cb #1d1240) as a thick halo */\n    halo: [\n      \"rgba(29,18,64,0)\",\n      \"rgba(29,18,64,0.55)\",\n      \"rgba(90,70,203,0.55)\",\n      \"rgba(157,180,255,0.5)\",\n      \"rgba(214,218,255,0.32)\",\n      \"rgba(90,70,203,0.36)\",\n      \"rgba(29,18,64,0.4)\",\n    ],\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    track: \"rgba(18,18,20,0.10)\",\n    stage: \"rgba(255,255,255,0.85)\",\n    push: \"rgba(255,255,255,0.75)\",\n    grain: 0.045,\n    halo: [\n      \"rgba(157,180,255,0)\",\n      \"rgba(157,180,255,0.16)\",\n      \"rgba(90,70,203,0.22)\",\n      \"rgba(157,180,255,0.55)\",\n      \"rgba(243,241,255,0.9)\",\n      \"rgba(157,180,255,0.3)\",\n      \"rgba(90,70,203,0.08)\",\n    ],\n  },\n} as const;\n\ntype Palette = (typeof THEMES)[keyof typeof THEMES];\ntype Exit = \"ring\" | \"slice\" | \"through\";\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `g${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, opacity }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={13}\n          stitchTiles=\"stitch\"\n        />\n        <feColorMatrix values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0.9 0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\n/** A centred monumental numeral; everything else is styling passed in. */\nfunction Numeral({\n  n,\n  size,\n  color,\n  style,\n}: {\n  n: number;\n  size: number;\n  color: string;\n  style?: React.CSSProperties;\n}) {\n  return (\n    <AbsoluteFill\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        ...style,\n      }}\n    >\n      <div\n        style={{\n          fontFamily: sans,\n          fontWeight: 400,\n          fontSize: size,\n          lineHeight: 1,\n          letterSpacing: \"-0.04em\",\n          color,\n          fontVariantNumeric: \"tabular-nums\",\n          // optical centring: Inter Tight figures sit high in the em box\n          transform: `translateY(${size * 0.035}px)`,\n        }}\n      >\n        {n}\n      </div>\n    </AbsoluteFill>\n  );\n}\n\nexport function Countdown({\n  from = 3,\n  finalLabel = \"Live\",\n  indicator = true,\n  caption = \"remocn 2.0\",\n  logo,\n  beat = 20,\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: CountdownProps) {\n  const t = useCurrentFrame() * speed;\n  const { width, height, fps } = useVideoConfig();\n  const u = height / 720;\n  const p: Palette = THEMES[theme];\n  const count = Math.max(1, Math.round(from));\n  const size = height * 0.66;\n  const ringR = height * 0.36;\n  const cxp = width / 2;\n  const cyp = height / 2;\n  const final = count * beat;\n\n  const exitOf = (k: number): Exit =>\n    k === count - 1 ? \"through\" : k % 2 === 0 ? \"ring\" : \"slice\";\n\n  const layers: React.ReactNode[] = [];\n  const overlays: React.ReactNode[] = [];\n\n  for (let k = 0; k < count; k++) {\n    const n = count - k;\n    const start = k * beat;\n    const end = start + beat;\n    if (t < start - beat || t > end + beat) continue;\n\n    // ---- entrance (driven by the previous numeral's exit) ----\n    const prevExit: Exit | null = k === 0 ? null : exitOf(k - 1);\n    let style: React.CSSProperties = {};\n    let opacity = 1;\n    let scale = 1;\n    let blur = 0;\n    let mask: string | undefined;\n\n    if (prevExit === null) {\n      const a = interpolate(t, [0, 9], [0, 1], { ...CLAMP, easing: EASE_OUT });\n      opacity = 0.3 + 0.7 * a;\n      scale = 1.05 - 0.05 * a;\n      blur = (1 - a) * 8 * u;\n    } else if (prevExit === \"ring\") {\n      const a = interpolate(t, [start - 12, start], [0, 360], {\n        ...CLAMP,\n        easing: EASE_IO,\n      });\n      if (t < start - 12) opacity = 0;\n      else if (t < start)\n        mask = `conic-gradient(from 0deg at 50% 50%, #000 0deg, #000 ${a}deg, transparent ${a + 0.8}deg)`;\n      const s = interpolate(t, [start - 12, start + 6], [1.04, 1], {\n        ...CLAMP,\n        easing: EASE_OUT,\n      });\n      scale = s;\n    } else if (prevExit === \"slice\") {\n      const a = interpolate(t, [start, start + 9], [0, 1], {\n        ...CLAMP,\n        easing: EASE_OUT,\n      });\n      opacity = t < start ? 0 : a;\n      scale = 1.14 - 0.14 * a;\n      blur = (1 - a) * 10 * u;\n    }\n\n    // a slow breath while the numeral holds\n    scale *= interpolate(t, [start, end], [1, 0.985], CLAMP);\n\n    // ---- exit ----\n    const exit = exitOf(k);\n    if (exit === \"ring\") {\n      const a = interpolate(t, [end - 12, end], [0, 360], {\n        ...CLAMP,\n        easing: EASE_IO,\n      });\n      if (t >= end) opacity = 0;\n      else if (t > end - 12)\n        mask = `conic-gradient(from 0deg at 50% 50%, transparent 0deg, transparent ${a}deg, #000 ${a + 0.8}deg)`;\n\n      // the ring itself: a hairline track and an accent sweep that leads the wipe\n      const ringIn = interpolate(t, [end - 16, end - 10], [0, 1], CLAMP);\n      const ringOut = interpolate(t, [end, end + 8], [1, 0], {\n        ...CLAMP,\n        easing: EASE_OUT,\n      });\n      const ringScale = interpolate(t, [end, end + 8], [1, 1.06], {\n        ...CLAMP,\n        easing: EASE_OUT,\n      });\n      const ro = ringIn * ringOut;\n      if (ro > 0) {\n        const circ = 2 * Math.PI * ringR;\n        const hand = ((a - 90) * Math.PI) / 180;\n        overlays.push(\n          <svg\n            key={`ring${k}`}\n            width={width}\n            height={height}\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              opacity: ro,\n              transform: `scale(${ringScale})`,\n            }}\n            aria-hidden\n          >\n            <circle\n              cx={cxp}\n              cy={cyp}\n              r={ringR}\n              fill=\"none\"\n              stroke={p.track}\n              strokeWidth={Math.max(1, 1.2 * u)}\n            />\n            <circle\n              cx={cxp}\n              cy={cyp}\n              r={ringR}\n              fill=\"none\"\n              stroke={accent}\n              strokeWidth={3 * u}\n              strokeLinecap=\"round\"\n              strokeDasharray={`${(circ * a) / 360} ${circ}`}\n              transform={`rotate(-90 ${cxp} ${cyp})`}\n            />\n            {a > 0 && a < 360 && (\n              <line\n                x1={cxp}\n                y1={cyp}\n                x2={cxp + Math.cos(hand) * ringR}\n                y2={cyp + Math.sin(hand) * ringR}\n                stroke={p.fg}\n                strokeOpacity={0.35}\n                strokeWidth={Math.max(1, u)}\n              />\n            )}\n          </svg>,\n        );\n      }\n    } else if (exit === \"slice\") {\n      const cut = interpolate(t, [end - 9, end - 3], [0, 1], {\n        ...CLAMP,\n        easing: EASE_IO,\n      });\n      const part = interpolate(t, [end - 3, end + 5], [0, 1], {\n        ...CLAMP,\n        easing: Easing.bezier(0.5, 0, 0.2, 1),\n      });\n      if (t >= end - 3) {\n        // replace the whole numeral with two halves that shear apart\n        const cutY = cyp + size * 0.02;\n        const shift = part * width * 0.42;\n        const gapY = part * size * 0.03;\n        const halves: Array<[string, number]> = [\n          [`inset(0 0 ${height - cutY}px 0)`, -1],\n          [`inset(${cutY}px 0 0 0)`, 1],\n        ];\n        for (const [clip, dir] of halves) {\n          // trailing ghosts: a fake motion blur along the shear\n          for (let g = 2; g >= 0; g--) {\n            const lag = g * 0.09;\n            const sh = Math.max(0, part - lag) / Math.max(0.001, 1 - lag);\n            layers.push(\n              <Numeral\n                key={`h${k}${dir}${g}`}\n                n={n}\n                size={size}\n                color={p.fg}\n                style={{\n                  clipPath: clip,\n                  transform: `translate(${dir * sh * shift}px, ${dir * gapY}px) scale(${scale})`,\n                  opacity:\n                    interpolate(part, [0.25, 1], [1, 0], CLAMP) *\n                    (g === 0\n                      ? 1\n                      : 0.2 * interpolate(part, [0.1, 0.35], [0, 1], CLAMP)),\n                  filter: g === 0 ? undefined : `blur(${3 * u}px)`,\n                }}\n              />,\n            );\n          }\n        }\n        opacity = 0;\n      }\n      const lineO =\n        interpolate(t, [end - 9, end - 7], [0, 1], CLAMP) *\n        interpolate(t, [end - 1, end + 6], [1, 0], CLAMP);\n      if (lineO > 0) {\n        const span = width * 0.62;\n        overlays.push(\n          <div\n            key={`cut${k}`}\n            style={{\n              position: \"absolute\",\n              left: cxp - span / 2,\n              top: cyp + size * 0.02 - u,\n              width: span,\n              height: Math.max(1.5, 2 * u),\n              background: accent,\n              transformOrigin: \"0% 50%\",\n              transform: `scaleX(${cut})`,\n              opacity: lineO,\n            }}\n          />,\n        );\n      }\n    } else {\n      // scale-through: the last numeral rushes past the camera\n      const r = interpolate(t, [end - 8, end + 2], [0, 1], CLAMP);\n      const grow = (x: number) => 1 + 9 * x * x * x;\n      if (r > 0) {\n        for (let g = 3; g >= 1; g--) {\n          const gr = Math.max(0, r - g * 0.06);\n          layers.push(\n            <Numeral\n              key={`g${k}${g}`}\n              n={n}\n              size={size}\n              color={p.fg}\n              style={{\n                transform: `scale(${scale * grow(gr)})`,\n                opacity:\n                  0.14 * interpolate(gr, [0, 0.2, 0.8], [0, 1, 0], CLAMP),\n                filter: `blur(${(3 + gr * gr * 18) * u}px)`,\n              }}\n            />,\n          );\n        }\n      }\n      scale *= grow(r);\n      opacity *= interpolate(r, [0.45, 0.95], [1, 0], CLAMP);\n      blur += r * r * 16 * u;\n    }\n\n    if (opacity > 0.001) {\n      style = {\n        opacity,\n        transform: `scale(${scale})`,\n        filter: blur > 0.2 ? `blur(${blur}px)` : undefined,\n        WebkitMaskImage: mask,\n        maskImage: mask,\n      };\n      layers.push(\n        <Numeral key={`n${k}`} n={n} size={size} color={p.fg} style={style} />,\n      );\n    }\n  }\n\n  // ---- final hit ----\n  const f = spring({\n    frame: t - final,\n    fps,\n    config: { damping: 16, stiffness: 160, mass: 0.8 },\n  });\n  const fO = interpolate(t, [final, final + 6], [0, 1], CLAMP);\n  const fBlur = interpolate(t, [final, final + 8], [10 * u, 0], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const dot = spring({\n    frame: t - final - 4,\n    fps,\n    config: { damping: 12, stiffness: 200, mass: 0.5 },\n  });\n  const push = interpolate(t, [final - 2, final + 2, final + 26], [0, 1, 0], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const cap = interpolate(t, [final + 12, final + 32], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const finalIsLogo = finalLabel.trim().toLowerCase() === \"remocn\";\n  const finalSize = Math.min(\n    height * 0.3,\n    (width * 0.7) / Math.max(3, Array.from(finalLabel).length * 0.56),\n  );\n  // rough rendered width of the final line, so the halo can open around it\n  const finalW = Math.max(\n    2 * ringR,\n    finalSize *\n      (finalIsLogo\n        ? 3.9\n        : Array.from(finalLabel).length * 0.55 + (indicator ? 0.4 : 0)),\n  );\n  // \"remocn 2.0\" -> the real logo followed by \"2.0\"\n  const capMatch = /^remocn\\b\\s*(.*)$/i.exec(caption);\n  const capText = capMatch ? capMatch[1] : caption;\n  const capLogo =\n    logo ??\n    (finalIsLogo && !capMatch ? null : (\n      <RemocnLogo fontSize={27 * u} color={p.fg} />\n    ));\n  // the whole stage leans in a touch at the cut, then settles\n  const stageKick =\n    1 + 0.02 * interpolate(t, [final, final + 3, final + 24], [0, 1, 0], CLAMP);\n\n  // the halo sits on the countdown ring's radius, breathes in as the count\n  // runs, and opens up at the hit to frame the final label\n  const haloOpen = interpolate(t, [final - 4, final + 30], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const haloScale =\n    interpolate(t, [0, final], [0.94, 1], CLAMP) *\n    (1 + (finalW / (2 * ringR * 0.9) - 1) * haloOpen);\n  const haloIn = interpolate(t, [0, 14], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const hs = (k: number) => `${(ringR * k).toFixed(1)}px`;\n  const h = p.halo;\n  const halo = `radial-gradient(circle at 50% 50%, ${h[0]} ${hs(0.78)}, ${h[1]} ${hs(0.88)}, ${h[2]} ${hs(0.96)}, ${h[3]} ${hs(0.995)}, ${h[4]} ${hs(1.01)}, ${h[5]} ${hs(1.07)}, ${h[6]} ${hs(1.2)}, transparent ${hs(1.55)})`;\n  // a slow light that travels around the halo\n  const sweep = 200 + t * 1.1;\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 60% 65% at 50% 38%, ${p.stage}, transparent 75%)`,\n        }}\n      />\n      <AbsoluteFill\n        style={{\n          background: halo,\n          opacity: haloIn,\n          transform: `scale(${haloScale})`,\n          WebkitMaskImage: `conic-gradient(from ${sweep}deg at 50% 50%, rgba(0,0,0,0.55), #000 40%, rgba(0,0,0,0.55) 70%, rgba(0,0,0,0.55))`,\n          maskImage: `conic-gradient(from ${sweep}deg at 50% 50%, rgba(0,0,0,0.55), #000 40%, rgba(0,0,0,0.55) 70%, rgba(0,0,0,0.55))`,\n        }}\n      />\n      <AbsoluteFill style={{ transform: `scale(${stageKick})` }}>\n        {layers}\n        {overlays}\n\n        {t >= final - 1 && (\n          <AbsoluteFill\n            style={{\n              display: \"flex\",\n              flexDirection: \"column\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              gap: 30 * u,\n            }}\n          >\n            <div\n              style={{\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: finalSize * 0.16,\n                opacity: fO,\n                filter: fBlur > 0.2 ? `blur(${fBlur}px)` : undefined,\n                transform: `scale(${0.9 + 0.1 * f})`,\n              }}\n            >\n              {indicator && (\n                <div\n                  style={{\n                    width: finalSize * 0.2,\n                    height: finalSize * 0.2,\n                    borderRadius: \"50%\",\n                    background: accent,\n                    transform: `scale(${0.5 + 0.5 * dot})`,\n                    marginTop: finalSize * 0.06,\n                  }}\n                />\n              )}\n              {finalIsLogo ? (\n                <RemocnLogo fontSize={finalSize} color={p.fg} />\n              ) : (\n                <div\n                  style={{\n                    fontFamily: sans,\n                    fontWeight: 500,\n                    fontSize: finalSize,\n                    lineHeight: 1,\n                    letterSpacing: \"-0.04em\",\n                    color: p.fg,\n                    whiteSpace: \"nowrap\",\n                  }}\n                >\n                  {finalLabel}\n                </div>\n              )}\n            </div>\n            {caption !== \"\" && (\n              <div\n                style={{\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  gap: 10 * u,\n                  opacity: cap,\n                  transform: `translateY(${(1 - cap) * 12 * u}px)`,\n                  fontFamily: sans,\n                  fontWeight: 400,\n                  fontSize: 24 * u,\n                  letterSpacing: \"-0.01em\",\n                  color: p.muted,\n                }}\n              >\n                {capLogo}\n                {capText}\n              </div>\n            )}\n          </AbsoluteFill>\n        )}\n      </AbsoluteFill>\n\n      {/* exposure push at the hit: light lifts, no white flash */}\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 70% 70% at 50% 50%, ${p.push}, transparent 80%)`,\n          opacity: push,\n        }}\n      />\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 80% 80% at 50% 50%, transparent 60%, ${p.bg} 100%)`,\n          opacity: 0.6,\n        }}\n      />\n      <Grain opacity={p.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/countdown.tsx"
    }
  ],
  "type": "registry:component"
}
