{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-flash",
  "title": "Split Flash",
  "description": "A lap-time clock freezes on its final value, flashing a purple bar and a minus delta.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/split-flash/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Geist\";\nimport { loadFont as loadMono } from \"@remotion/google-fonts/GeistMono\";\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: [\"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface SplitFlashProps {\n  /** The time the clock freezes on, `m:ss.mmm`. Anything else is shown static. */\n  time?: string;\n  /** Delta that drops in under the frozen time. Pass \"\" to hide. */\n  delta?: string;\n  /** Small label above the clock while it runs. */\n  label?: string;\n  /** Label once the clock freezes. */\n  flashLabel?: string;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\n// Geist 500 with tabular figures, per 1000 em (harfbuzz). Digits share one\n// advance, so the running clock never shifts.\nconst DIGIT = 0.612;\nconst COLON = 0.302;\nconst DOT = 0.213;\nconst OTHER = 0.55;\nconst CAP = 0.71;\nconst TRACK = -0.028;\n/** Left and right side bearings of the digits 0–9, per 1000 em. */\nconst DIGIT_LSB = [38, 98, 51, 42, 30, 45, 47, 51, 32, 47];\nconst DIGIT_RSB = [38, 98, 49, 41, 29, 44, 46, 51, 32, 46];\n\n// Beat sheet (30fps, speed 1):\n//  0–42    the lap clock runs: tenths roll, the last digits flicker, a thin\n//          accent sector line draws under it and the clock creeps closer\n//  42–44   the line is crossed: the clock freezes on the final time and the\n//          sector line shoots up into a full-bleed purple band behind it\n//          (\"fastest\" purple, gradients.sh void palette); the label flips\n//  46–48   the band blinks off for two frames and back on: the timing flash\n//  50–59   the delta slides down out from under the band\n//  58–     poster hold\nconst RUN = 42;\nconst GROW_LEN = 2;\nconst BLINK_AT = 4;\nconst BLINK_LEN = 2;\nconst DELTA_AT = 8;\nconst DELTA_LEN = 9;\n\nconst PUSH = Easing.bezier(0.45, 0, 0.8, 0.5);\nconst DROP = Easing.bezier(0.2, 0.85, 0.3, 1);\nconst GROW = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n// gradients.sh \"void\": #c9b6ff, #7b4dff, #2c1670.\nconst VOID = [\"#C9B6FF\", \"#7B4DFF\", \"#2C1670\"] as const;\n\nconst THEMES = {\n  dark: {\n    stage: \"#0A0A0B\",\n    ink: \"#F3F2EF\",\n    dim: \"#77767C\",\n    line: \"#1C1C20\",\n    flash: VOID[0],\n  },\n  light: {\n    stage: \"#F5F4F0\",\n    ink: \"#121214\",\n    dim: \"#8A8983\",\n    line: \"#E2E0DA\",\n    flash: \"#5A35D6\",\n  },\n} as const;\n\nfunction cell(ch: string) {\n  if (ch >= \"0\" && ch <= \"9\") return DIGIT + TRACK;\n  if (ch === \":\") return COLON + TRACK;\n  if (ch === \".\") return DOT + 0.04 + TRACK;\n  return OTHER + TRACK;\n}\n\nfunction parseTime(s: string) {\n  const m = /^(\\d+):(\\d{2})\\.(\\d{3})$/.exec(s);\n  if (!m) return null;\n  return Number(m[1]) * 60000 + Number(m[2]) * 1000 + Number(m[3]);\n}\n\nfunction hash(n: number) {\n  const x = Math.sin(n * 127.1 + 311.7) * 43758.5453;\n  return x - Math.floor(x);\n}\n\n/** Running clock face for `ms`; the thousandths flicker like a live timer. */\nfunction formatTime(ms: number, frame: number, frozen: boolean) {\n  const clamped = Math.max(0, ms);\n  const min = Math.floor(clamped / 60000);\n  const sec = Math.floor((clamped % 60000) / 1000);\n  const milli = Math.floor(clamped % 1000);\n  const last = frozen ? milli % 10 : Math.floor(hash(frame) * 10);\n  const body = Math.floor(milli / 10) * 10 + last;\n  return `${min}:${String(sec).padStart(2, \"0\")}.${String(body).padStart(3, \"0\")}`;\n}\n\nexport function SplitFlash({\n  time = \"1:12.408\",\n  delta = \"−0.412\",\n  label = \"Lap time\",\n  flashLabel = \"Fastest lap\",\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: SplitFlashProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n\n  const finalMs = parseTime(time);\n  const frozen = t >= RUN || finalMs === null;\n  const face =\n    finalMs === null\n      ? time\n      : formatTime(\n          finalMs - (Math.max(0, RUN - t) * 1000) / 30,\n          Math.floor(t),\n          frozen,\n        );\n\n  // Layout from the final face so the running one sits in the same cells.\n  const cells = time.split(\"\").map(cell);\n  const advance = cells.reduce((a, b) => a + b, 0) - TRACK;\n  const first = time[0] ?? \"0\";\n  const lastCh = time[time.length - 1] ?? \"0\";\n  const lsb =\n    first >= \"0\" && first <= \"9\" ? DIGIT_LSB[Number(first)] / 1000 : 0;\n  const rsb =\n    lastCh >= \"0\" && lastCh <= \"9\" ? DIGIT_RSB[Number(lastCh)] / 1000 : 0;\n  const inkEm = advance - lsb - rsb;\n  const fs = Math.min((0.8 * W) / inkEm, (0.3 * H) / CAP);\n  const cap = CAP * fs;\n  const inkW = inkEm * fs;\n  const inkL = (W - inkW) / 2;\n  const penX = inkL - lsb * fs;\n\n  const bandH = cap * 1.72;\n  const deltaFs = 0.36 * fs;\n  const labelFs = Math.max(13, 0.021 * W);\n  // Block: label, band, delta. Centered with the band slightly above center.\n  const bandY = (H - bandH) / 2 - 0.035 * H;\n  const baseline = bandY + (bandH + cap) / 2;\n  const bandBottom = bandY + bandH;\n\n  const since = t - RUN;\n  const bandOn =\n    frozen &&\n    finalMs !== null &&\n    !(since >= BLINK_AT && since < BLINK_AT + BLINK_LEN);\n  const progress = finalMs === null ? 1 : Math.min(1, Math.max(0, t / RUN));\n  const push = 0.955 + 0.045 * PUSH(progress);\n  const drop = DROP(\n    interpolate(since, [DELTA_AT, DELTA_AT + DELTA_LEN], [0, 1], CLAMP),\n  );\n\n  const lineH = Math.max(3, 0.0065 * H);\n  // The band grows up out of the sector line, then blinks.\n  const grow = GROW(interpolate(since, [0, GROW_LEN], [0, 1], CLAMP));\n  const bandNowH = lineH + (bandH - lineH) * grow;\n  // Gentle sheen drift across the band while it holds.\n  const drift = interpolate(since, [0, 60], [-0.08, 0.04], CLAMP);\n  const onBand = bandOn ? \"#F7F5FF\" : c.ink;\n  const cx = W / 2;\n  const cy = bandY + bandH / 2;\n\n  let pen = penX;\n  const glyphs = face.split(\"\").map((ch, i) => {\n    const x = pen;\n    pen += (cells[i] ?? cell(ch)) * fs;\n    return { ch, x };\n  });\n\n  return (\n    <AbsoluteFill style={{ background: c.stage }}>\n      <svg\n        width={W}\n        height={H}\n        viewBox={`0 0 ${W} ${H}`}\n        style={{ position: \"absolute\", inset: 0 }}\n        role=\"img\"\n        aria-label={`${frozen ? flashLabel : label} ${time} ${delta}`}\n      >\n        <defs>\n          <linearGradient\n            id={`${uid}-band`}\n            x1=\"0\"\n            y1=\"0\"\n            x2=\"1\"\n            y2=\"0.35\"\n            gradientTransform={`translate(${drift} 0)`}\n          >\n            <stop offset=\"0\" stopColor={VOID[2]} />\n            <stop offset=\"0.55\" stopColor={VOID[1]} />\n            <stop offset=\"1\" stopColor=\"#9F80FF\" />\n          </linearGradient>\n          <clipPath id={`${uid}-under`}>\n            <rect x={0} y={bandBottom} width={W} height={H} />\n          </clipPath>\n        </defs>\n        {bandOn ? (\n          <rect\n            x={0}\n            y={bandBottom - bandNowH}\n            width={W}\n            height={bandNowH}\n            fill={`url(#${uid}-band)`}\n          />\n        ) : null}\n        <g\n          transform={`translate(${cx} ${cy}) scale(${frozen ? 1 : push}) translate(${-cx} ${-cy})`}\n        >\n          {!frozen ? (\n            <>\n              <rect\n                x={inkL}\n                y={bandBottom - lineH}\n                width={inkW}\n                height={lineH}\n                fill={c.line}\n              />\n              <rect\n                x={inkL}\n                y={bandBottom - lineH}\n                width={inkW * progress}\n                height={lineH}\n                fill={accent}\n              />\n            </>\n          ) : null}\n          <text\n            x={inkL}\n            y={bandY - 0.5 * labelFs}\n            fill={frozen ? c.flash : c.dim}\n            fontFamily={mono}\n            fontWeight={500}\n            fontSize={labelFs}\n            style={{ letterSpacing: \"0.08em\", textTransform: \"uppercase\" }}\n          >\n            {frozen ? flashLabel : label}\n          </text>\n          {glyphs.map((g, i) => (\n            <text\n              key={i}\n              x={g.x}\n              y={baseline}\n              fill={onBand}\n              fontFamily={display}\n              fontWeight={500}\n              fontSize={fs}\n              style={{ fontFeatureSettings: '\"tnum\" 1', fontKerning: \"none\" }}\n            >\n              {g.ch}\n            </text>\n          ))}\n        </g>\n        {delta && drop > 0 ? (\n          <g clipPath={`url(#${uid}-under)`}>\n            <text\n              x={inkL + inkW}\n              y={\n                bandBottom + deltaFs * (0.28 + CAP) - deltaFs * 1.1 * (1 - drop)\n              }\n              textAnchor=\"end\"\n              fill={c.flash}\n              fontFamily={display}\n              fontWeight={500}\n              fontSize={deltaFs}\n              style={{\n                fontFeatureSettings: '\"tnum\" 1',\n                letterSpacing: \"-0.02em\",\n              }}\n            >\n              {delta}\n            </text>\n          </g>\n        ) : null}\n      </svg>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/split-flash.tsx"
    }
  ],
  "type": "registry:component"
}
