{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drop-timer",
  "title": "Drop Timer",
  "description": "A countdown to a drop ends in a huge gradient 'LIVE NOW' with a URL and logo.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/drop-timer/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/BigShoulders\";\nimport { loadFont as loadMono } from \"@remotion/google-fonts/GeistMono\";\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  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: display } = loadFont(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: mono } = loadMono(\"normal\", {\n  weights: [\"400\"],\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 DropTimerProps {\n  /** Small line over the countdown. */\n  label?: string;\n  /** Seconds on the clock when the shot opens; it counts down to zero. */\n  from?: number;\n  /** Frames per counted second (a film second, not a real one). */\n  tick?: number;\n  /** Unit captions under the three digit pairs. */\n  units?: [string, string, string];\n  /** What the clock smashes into at zero, set full-bleed. */\n  live?: string;\n  /** Brand over the payoff; \"remocn\" renders the real logo. */\n  name?: string;\n  /** Replaces the logo / name over the payoff. */\n  logo?: React.ReactNode;\n  /** Line under the payoff. */\n  url?: string;\n  /** The small \"live\" dot beside the url. */\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\n// Big Shoulders 500 (fontTools, wght 500): cap 0.8; advances in em.\n// Digits sit in fixed 0.46em cells (true tabular), colons in 0.24em cells.\nconst CAP = 0.8;\nconst CELL = 0.46;\nconst COLON = 0.24;\nconst ADV: Record<string, number> = {\n  L: 0.359,\n  I: 0.2175,\n  V: 0.4405,\n  E: 0.3845,\n  N: 0.541,\n  O: 0.457,\n  W: 0.7235,\n  \" \": 0.212,\n  D: 0.453,\n  R: 0.4395,\n  P: 0.4295,\n  S: 0.438,\n};\nconst LSB: Record<string, number> = { L: 0.056, D: 0.056, I: 0.056 };\nconst RSB: Record<string, number> = { W: 0.0295, N: 0.056, E: 0.0275 };\nconst TRACK_LIVE = -0.01;\n\nfunction inkWidth(s: string, track: number) {\n  const chars = Array.from(s);\n  let w = 0;\n  for (const ch of chars) w += ADV[ch] ?? 0.38;\n  w += track * Math.max(0, chars.length - 1);\n  const first = chars[0] ?? \" \";\n  const last = chars[chars.length - 1] ?? \" \";\n  return w - (LSB[first] ?? 0.04) - (RSB[last] ?? 0.04);\n}\n\n// Beat sheet (30fps, speed 1, from 5, tick 14):\n//   0–6    the clock punches in at 00:00:05 from 1.3× and a blur\n//   6–76   every 14 frames: a one-frame inverted flash, a hard cut to the next\n//          digit and a new ground colour from the palette, a bump and a shake\n//  76–84   00:00:00 holds on black, the digits lit in the gradient\n//  84–90   smash: the clock rushes past the lens, LIVE NOW slams in full-bleed\n//  92–     logo and url rise; poster hold, the gradient drifts\nconst START = 6;\nconst ZERO_HOLD = 8;\n\nconst SLAM = Easing.bezier(0.22, 1, 0.36, 1);\nconst RUSH = Easing.bezier(0.6, 0, 0.9, 0.4);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface Ground {\n  bg: string;\n  ink: string;\n}\n\n// Dark: gradients.sh \"fluted\" (bg #050507): #1d2a8f #2f7bff #ff7a3d #ffe2c4.\n// Light: gradients.sh \"ribbon\" (bg #ffffff): #ff3d9a #ff8a3d #ffc83d #7a5cff.\nconst THEMES = {\n  dark: {\n    stage: \"#0A0A0B\",\n    ink: \"#F3F2EF\",\n    flash: \"#F3F2EF\",\n    flashInk: \"#0A0A0B\",\n    grounds: [\n      {\n        bg: \"radial-gradient(70% 90% at 80% 10%, #1d2a8f 0%, transparent 70%), #0A0A0B\",\n        ink: \"#F3F2EF\",\n      },\n      {\n        bg: \"radial-gradient(80% 110% at 18% 0%, #8fb6ff 0%, transparent 62%), #2f7bff\",\n        ink: \"#F3F2EF\",\n      },\n      {\n        bg: \"radial-gradient(80% 110% at 85% 100%, #ffe2c4 0%, transparent 62%), #ff7a3d\",\n        ink: \"#0A0A0B\",\n      },\n      {\n        bg: \"radial-gradient(90% 110% at 10% 100%, #ffb98a 0%, transparent 60%), #ffe2c4\",\n        ink: \"#0A0A0B\",\n      },\n      {\n        bg: \"radial-gradient(80% 110% at 82% 0%, #2f7bff 0%, transparent 64%), #1d2a8f\",\n        ink: \"#F3F2EF\",\n      },\n    ] as Ground[],\n    zero: \"#050507\",\n    hero: [\"#ffe2c4\", \"#ff7a3d\", \"#2f7bff\"],\n    haze: \"#1d2a8f\",\n    hazeHi: \"#2f7bff\",\n    hazeAlpha: 0.9,\n  },\n  light: {\n    stage: \"#F5F4F0\",\n    ink: \"#121214\",\n    flash: \"#121214\",\n    flashInk: \"#F5F4F0\",\n    grounds: [\n      {\n        bg: \"radial-gradient(70% 90% at 80% 10%, #ffe9f3 0%, transparent 70%), #F5F4F0\",\n        ink: \"#121214\",\n      },\n      {\n        bg: \"radial-gradient(80% 110% at 18% 0%, #b8a6ff 0%, transparent 62%), #7a5cff\",\n        ink: \"#F5F4F0\",\n      },\n      {\n        bg: \"radial-gradient(80% 110% at 85% 100%, #ff8a3d 0%, transparent 62%), #ff3d9a\",\n        ink: \"#121214\",\n      },\n      {\n        bg: \"radial-gradient(90% 110% at 10% 100%, #ff8a3d 0%, transparent 60%), #ffc83d\",\n        ink: \"#121214\",\n      },\n      {\n        bg: \"radial-gradient(80% 110% at 82% 0%, #ffc83d 0%, transparent 64%), #ff8a3d\",\n        ink: \"#121214\",\n      },\n    ] as Ground[],\n    zero: \"#F5F4F0\",\n    hero: [\"#7a5cff\", \"#ff3d9a\", \"#ff8a3d\"],\n    haze: \"#ff3d9a\",\n    hazeHi: \"#ffc83d\",\n    hazeAlpha: 0.22,\n  },\n} as const;\n\nfunction ramp(t: number, from: number, len: number) {\n  return interpolate(t, [from, from + len], [0, 1], CLAMP);\n}\n\n/** A short hit: up in 2 frames, eases back over 8. */\nfunction kick(d: number) {\n  if (d < 0) return 0;\n  if (d < 2) return d / 2;\n  const k = Math.max(0, 1 - (d - 2) / 8);\n  return k * k;\n}\n\n/** A decaying shake that dies in 7 frames. Deterministic. */\nfunction shake(d: number, amp: number, seed: number) {\n  if (d < 0 || d > 7) return { x: 0, y: 0 };\n  const k = (1 - d / 7) ** 2;\n  return {\n    x: amp * k * Math.sin(d * 2.7 + seed),\n    y: amp * 0.7 * k * Math.cos(d * 3.3 + seed * 1.7),\n  };\n}\n\nconst pad = (n: number) => String(Math.max(0, Math.floor(n))).padStart(2, \"0\");\n\nexport function DropTimer({\n  label = \"DROPS IN\",\n  from = 5,\n  tick = 14,\n  units = [\"HRS\", \"MIN\", \"SEC\"],\n  live = \"LIVE NOW\",\n  name = \"remocn\",\n  logo,\n  url = \"remocn.dev\",\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: DropTimerProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const u = Math.min(W / 1280, H / 720);\n  const c = THEMES[theme];\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n\n  const total = Math.max(1, Math.round(from));\n  const zeroAt = START + total * tick;\n  const smashAt = zeroAt + ZERO_HOLD;\n\n  // ── Clock state ──\n  const elapsed = Math.max(0, Math.floor((t - START) / tick));\n  const left = Math.max(0, total - elapsed);\n  const tickIdx = Math.min(total, elapsed);\n  const lastTick = START + tickIdx * tick;\n  const sinceTick = t - lastTick;\n  const isFlash = t >= START + tick && sinceTick >= 0 && sinceTick < 1;\n  const atZero = left === 0;\n  const ground = c.grounds[tickIdx % c.grounds.length];\n\n  const hh = Math.floor(left / 3600);\n  const mm = Math.floor((left % 3600) / 60);\n  const ss = left % 60;\n  const clock = `${pad(hh)}:${pad(mm)}:${pad(ss)}`;\n\n  // ── Clock layout: fixed cells, fitted to the frame width ──\n  const cells = Array.from(clock).map((ch) => (ch === \":\" ? COLON : CELL));\n  const clockEm = cells.reduce((a, b) => a + b, 0);\n  const fs = Math.min((0.9 * W) / clockEm, (0.56 * H) / CAP);\n  const clockW = clockEm * fs;\n  const clockX = (W - clockW) / 2;\n  const capH = CAP * fs;\n  const baseline = H * 0.5 + capH * 0.5 + 0.02 * H;\n  let cx = clockX;\n  const glyphs = Array.from(clock).map((ch, i) => {\n    const mid = cx + (cells[i] * fs) / 2;\n    cx += cells[i] * fs;\n    return { ch, mid, i };\n  });\n  const pairMid = [0, 3, 6].map((i) => (glyphs[i].mid + glyphs[i + 1].mid) / 2);\n\n  // ── Camera ──\n  const enter = SLAM(ramp(t, 0, START));\n  const enterScale = 1.3 - 0.3 * enter;\n  const enterBlur = (1 - enter) * 16;\n  let bump = 0;\n  let sx = 0;\n  let sy = 0;\n  for (let k = 1; k <= total; k++) {\n    const d = t - (START + k * tick);\n    if (d < -1 || d > 12) continue;\n    // hits grow as the clock runs out\n    const g = 0.7 + (0.6 * k) / total;\n    bump = Math.max(bump, g * kick(d));\n    const s = shake(d, 6 * u * g, k * 1.3);\n    sx += s.x;\n    sy += s.y;\n  }\n  const smashD = t - smashAt;\n  bump = Math.max(bump, 1.4 * kick(smashD - 4));\n  const s3 = shake(smashD - 4, 12 * u, 4.4);\n  sx += s3.x;\n  sy += s3.y;\n  const camScale = 1 + 0.04 * bump;\n\n  // ── Smash ──\n  const rush = RUSH(ramp(t, smashAt, 5));\n  const clockScale = enterScale * (1 + 1.8 * rush);\n  const clockBlur = enterBlur + 26 * rush;\n  const clockOpacity = Math.min(ramp(t, 0, 2), 1 - ramp(t, smashAt + 2, 3));\n  const liveP = SLAM(ramp(t, smashAt + 1, 7));\n  const liveScale = 1.7 - 0.7 * liveP;\n  const liveBlur = (1 - liveP) * 18;\n  const liveOpacity = ramp(t, smashAt + 1, 2);\n  const flashSmash = interpolate(\n    t,\n    [smashAt, smashAt + 1, smashAt + 5],\n    [0, 1, 0],\n    CLAMP,\n  );\n  const after = t >= smashAt + 1;\n  const rise = (a: number) => SLAM(ramp(t, a, 16));\n  const drift = ramp(t, smashAt, 70);\n  const haze = SLAM(ramp(t, smashAt + 1, 24));\n\n  // ── Payoff layout ──\n  const liveEm = inkWidth(live.toUpperCase(), TRACK_LIVE);\n  const liveFs = Math.min((0.9 * W) / liveEm, (0.5 * H) / CAP);\n  const liveCap = CAP * liveFs;\n  const liveBase = H * 0.535 + liveCap * 0.5;\n\n  const bgNow = after\n    ? c.zero\n    : isFlash\n      ? c.flash\n      : atZero\n        ? c.zero\n        : ground.bg;\n  const digitInk = isFlash ? c.flashInk : ground.ink;\n  const labelInk = isFlash ? c.flashInk : atZero ? c.ink : ground.ink;\n\n  const heroGrad = `${uid}hero`;\n  const zeroGrad = `${uid}zero`;\n\n  return (\n    <AbsoluteFill style={{ background: c.stage, overflow: \"hidden\" }}>\n      <div style={{ position: \"absolute\", inset: 0, background: bgNow }} />\n      {/* Payoff haze rising from the bottom. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          opacity: after ? haze * c.hazeAlpha : 0,\n          background: [\n            `radial-gradient(60% 50% at ${44 + 10 * drift}% ${122 - 18 * haze}%, ${c.hazeHi} 0%, transparent 70%)`,\n            `radial-gradient(95% 70% at 50% ${130 - 16 * haze}%, ${c.haze} 0%, transparent 76%)`,\n          ].join(\", \"),\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `translate(${sx}px, ${sy}px) scale(${camScale})`,\n          transformOrigin: \"50% 50%\",\n        }}\n      >\n        <svg\n          width={W}\n          height={H}\n          viewBox={`0 0 ${W} ${H}`}\n          style={{ position: \"absolute\", inset: 0, overflow: \"visible\" }}\n          role=\"img\"\n          aria-label={after ? `${live} — ${name} ${url}` : `${label} ${clock}`}\n        >\n          <defs>\n            <linearGradient\n              id={zeroGrad}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={clockX}\n              y1={baseline - capH}\n              x2={clockX + clockW}\n              y2={baseline}\n            >\n              <stop offset=\"0\" stopColor={c.hero[2]} />\n              <stop offset=\"0.55\" stopColor={c.hero[1]} />\n              <stop offset=\"1\" stopColor={c.hero[0]} />\n            </linearGradient>\n            <linearGradient\n              id={heroGrad}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={W * (0.02 + 0.06 * drift)}\n              y1={liveBase}\n              x2={W * (0.92 + 0.06 * drift)}\n              y2={liveBase - liveCap}\n            >\n              <stop offset=\"0\" stopColor={c.hero[1]} />\n              <stop offset=\"0.62\" stopColor={c.hero[0]} />\n              <stop offset=\"1\" stopColor={c.hero[0]} />\n            </linearGradient>\n          </defs>\n\n          {/* The clock. */}\n          {t < smashAt + 6 ? (\n            <g\n              style={{\n                transform: `scale(${clockScale})`,\n                transformOrigin: `${W / 2}px ${baseline - capH / 2}px`,\n                filter:\n                  clockBlur > 0.2 ? `blur(${clockBlur * u}px)` : undefined,\n              }}\n              opacity={clockOpacity}\n            >\n              <text\n                x={W / 2}\n                y={baseline - capH - 0.075 * H}\n                textAnchor=\"middle\"\n                fontFamily={display}\n                fontWeight={500}\n                fontSize={0.068 * H}\n                fill={labelInk}\n                style={{ letterSpacing: \"0.08em\" }}\n              >\n                {label}\n              </text>\n              <g\n                fontFamily={display}\n                fontWeight={500}\n                fontSize={fs}\n                textAnchor=\"middle\"\n                fill={atZero && !isFlash ? `url(#${zeroGrad})` : digitInk}\n              >\n                {glyphs.map((g) => (\n                  <text\n                    key={g.i}\n                    x={g.mid}\n                    y={g.ch === \":\" ? baseline - 0.035 * fs : baseline}\n                  >\n                    {g.ch}\n                  </text>\n                ))}\n              </g>\n              <g\n                fontFamily={mono}\n                fontWeight={400}\n                fontSize={0.03 * H}\n                textAnchor=\"middle\"\n                fill={labelInk}\n                opacity={0.8}\n                style={{ letterSpacing: \"0.14em\" }}\n              >\n                {units.map((un, i) => (\n                  <text key={un} x={pairMid[i]} y={baseline + 0.075 * H}>\n                    {un}\n                  </text>\n                ))}\n              </g>\n            </g>\n          ) : null}\n\n          {/* LIVE NOW, full-bleed. */}\n          {after ? (\n            <text\n              x={W / 2}\n              y={liveBase}\n              textAnchor=\"middle\"\n              fontFamily={display}\n              fontWeight={500}\n              fontSize={liveFs}\n              fill={`url(#${heroGrad})`}\n              opacity={liveOpacity}\n              style={{\n                letterSpacing: `${TRACK_LIVE}em`,\n                transform: `scale(${liveScale})`,\n                transformOrigin: `${W / 2}px ${liveBase - liveCap / 2}px`,\n                filter: liveBlur > 0.2 ? `blur(${liveBlur * u}px)` : undefined,\n              }}\n            >\n              {live.toUpperCase()}\n            </text>\n          ) : null}\n        </svg>\n\n        {/* Brand over the payoff. */}\n        {after ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              top: liveBase - liveCap - 0.135 * H,\n              display: \"flex\",\n              justifyContent: \"center\",\n              opacity: rise(smashAt + 7),\n              transform: `translateY(${(1 - rise(smashAt + 7)) * 14 * u}px)`,\n            }}\n          >\n            {logo ??\n              (name.trim().toLowerCase() === \"remocn\" ? (\n                <RemocnLogo fontSize={0.064 * H} color={c.ink} />\n              ) : (\n                <span\n                  style={{\n                    fontFamily: display,\n                    fontWeight: 500,\n                    fontSize: 0.064 * H,\n                    color: c.ink,\n                    lineHeight: 1,\n                  }}\n                >\n                  {name}\n                </span>\n              ))}\n          </div>\n        ) : null}\n\n        {/* Url under the payoff. */}\n        {after ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              top: liveBase + 0.075 * H,\n              display: \"flex\",\n              justifyContent: \"center\",\n              alignItems: \"center\",\n              gap: 12 * u,\n              fontFamily: mono,\n              fontWeight: 400,\n              fontSize: 0.032 * H,\n              letterSpacing: \"0.04em\",\n              color: c.ink,\n              lineHeight: 1,\n              opacity: rise(smashAt + 11),\n              transform: `translateY(${(1 - rise(smashAt + 11)) * 14 * u}px)`,\n            }}\n          >\n            <div\n              style={{\n                width: 0.016 * H,\n                height: 0.016 * H,\n                borderRadius: 99,\n                background: accent,\n                opacity:\n                  0.8 + 0.2 * Math.cos(((t - smashAt) / 22) * Math.PI * 2),\n              }}\n            />\n            {url}\n          </div>\n        ) : null}\n      </div>\n\n      {/* The smash flash. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: c.flash,\n          opacity: flashSmash,\n        }}\n      />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/drop-timer.tsx"
    }
  ],
  "type": "registry:component"
}
