{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flash-count",
  "title": "Flash Count",
  "description": "A counter lands on 3,600 with a camera-flash pop, an exposure eyebrow in the corner.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/flash-count/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadHanken } from \"@remotion/google-fonts/HankenGrotesk\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  interpolateColors,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst hanken = loadHanken(\"normal\", {\n  weights: [\"300\", \"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface FlashCountProps {\n  value?: number;\n  from?: number;\n  decimals?: number;\n  prefix?: string;\n  suffix?: string;\n  label?: string;\n  eyebrow?: string;\n  flashAt?: number;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: {\n    bg: \"#1A1918\",\n    ambient: \"#131211\",\n    pop: \"#D9D6D0\",\n    dim: \"#57534E\",\n    ink: \"#D9D6D0\",\n    muted: \"#8F8A83\",\n    shadow: \"#0B0A0A\",\n  },\n  light: {\n    bg: \"#D9D6D0\",\n    ambient: \"#C4C0B9\",\n    pop: \"#F6F4F0\",\n    dim: \"#9C978F\",\n    ink: \"#111111\",\n    muted: \"#57534E\",\n    shadow: \"rgba(17,17,17,0.2)\",\n  },\n} as const;\n\nconst RED = \"#E2001A\";\nconst TRACK = -0.04;\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface Metrics {\n  adv: Record<string, number>;\n  digit: number;\n  cap: number;\n  label: number;\n  eyebrow: number;\n}\n\nfunction measure(chars: string[], label: string, eyebrow: string): Metrics {\n  const ctx = document.createElement(\"canvas\").getContext(\"2d\");\n  if (!ctx) throw new Error(\"flash-count: no 2d context\");\n  ctx.font = `300 100px \"${hanken.fontFamily}\"`;\n  const adv: Record<string, number> = {};\n  for (const ch of chars) adv[ch] = ctx.measureText(ch).width / 100;\n  let digit = 0;\n  for (const d of \"0123456789\") digit = Math.max(digit, adv[d] ?? 0);\n  const cap = ctx.measureText(\"8\").actualBoundingBoxAscent / 100;\n  ctx.font = `400 100px \"${hanken.fontFamily}\"`;\n  const lw = label ? ctx.measureText(label).width / 100 : 0;\n  ctx.font = `500 100px \"${hanken.fontFamily}\"`;\n  const ew = eyebrow ? ctx.measureText(eyebrow).width / 100 : 0;\n  return { adv, digit, cap, label: lw, eyebrow: ew };\n}\n\nfunction useMetrics(chars: string, label: string, eyebrow: string) {\n  const key = `${chars}\\u0001${label}\\u0001${eyebrow}`;\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"flash-count: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      hanken.waitUntilDone(),\n      document.fonts.load(`300 100px \"${hanken.fontFamily}\"`),\n      document.fonts.load(`400 100px \"${hanken.fontFamily}\"`),\n      document.fonts.load(`500 100px \"${hanken.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        const [c, l, e] = key.split(\"\\u0001\");\n        setState({ key, m: measure(Array.from(c), l, e) });\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [key, handle]);\n  return state && state.key === key ? state.m : null;\n}\n\nfunction format(n: number, decimals: number) {\n  return n.toLocaleString(\"en-US\", {\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n  });\n}\n\nconst isDigit = (ch: string) => ch >= \"0\" && ch <= \"9\";\n\nexport function FlashCount({\n  value = 3600,\n  from = 120,\n  decimals = 0,\n  prefix = \"\",\n  suffix = \"\",\n  label = \"frames in a two\\u2011minute launch film\",\n  eyebrow = \"f/8  ·  1/250 s\",\n  flashAt = 54,\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: FlashCountProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const dot = accent ?? RED;\n  const transparent = background === \"transparent\";\n\n  const finalText = `${prefix}${format(value, decimals)}${suffix}`;\n  const chars = Array.from(\n    new Set(Array.from(`${finalText}0123456789,.-`)),\n  ).join(\"\");\n  const m = useMetrics(chars, label, eyebrow);\n\n  const F_ = flashAt;\n  const lit = t >= F_;\n  const pop = lit\n    ? interpolate(t, [F_, F_ + 1, F_ + 12], [1, 0.85, 0], {\n        ...CLAMP,\n        easing: EASE_OUT,\n      })\n    : 0;\n  const pre = interpolate(t, [F_ - 8, F_ - 7, F_ - 5], [0, 1, 0], CLAMP);\n  const field = lit\n    ? interpolateColors(pop, [0, 1], [c.bg, c.pop])\n    : interpolateColors(pre * 0.25, [0, 1], [c.ambient, c.pop]);\n  const stage = transparent ? \"transparent\" : (background ?? field);\n\n  if (!m) {\n    return (\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: transparent ? \"transparent\" : (background ?? c.ambient),\n        }}\n      />\n    );\n  }\n\n  const countP = interpolate(t, [4, F_], [0, 1], {\n    ...CLAMP,\n    easing: Easing.bezier(0.3, 0.6, 0.4, 1),\n  });\n  const n = lit ? value : from + (value - from) * countP;\n  const shown = `${prefix}${format(\n    lit ? value : Number(n.toFixed(decimals)),\n    decimals,\n  )}${suffix}`;\n\n  const widthOf = (s: string) => {\n    let w = 0;\n    const list = Array.from(s);\n    for (const ch of list) w += isDigit(ch) ? m.digit : (m.adv[ch] ?? m.digit);\n    return w + TRACK * Math.max(0, list.length - 1);\n  };\n\n  const portrait = H > W * 1.1;\n  const minDim = Math.min(W, H);\n  const marginX = W * (portrait ? 0.09 : 0.08);\n  const marginY = H * (portrait ? 0.07 : 0.12);\n  const F = Math.min(\n    (H * (portrait ? 0.15 : 0.34)) / m.cap,\n    (W * (portrait ? 0.82 : 0.56)) / widthOf(finalText),\n  );\n  const capPx = m.cap * F;\n  const smallFs = Math.max(minDim * 0.026, Math.min(minDim * 0.032, 24));\n\n  const baseline = H - marginY;\n  const right = W - marginX;\n\n  const driftP = Math.min(1, t / F_);\n  const drift = 1 - driftP;\n  const dx = drift * -W * 0.05;\n  const ds = 1 - drift * 0.06;\n\n  const shadowDx = F * 0.03;\n  const shadowDy = F * 0.045;\n\n  const textW = widthOf(shown) * F;\n  const left = right - textW;\n  const labelW = portrait\n    ? W - marginX * 2\n    : Math.max(W * 0.2, right - widthOf(finalText) * F - marginX - W * 0.04);\n\n  const glyphs = (key: string) => {\n    let x = 0;\n    return Array.from(shown).map((ch, i) => {\n      const cell = isDigit(ch) ? m.digit : (m.adv[ch] ?? m.digit);\n      const own = m.adv[ch] ?? cell;\n      const at = x + (cell - own) / 2;\n      x += cell + TRACK;\n      return (\n        <span\n          key={`${key}${i}`}\n          style={{ position: \"absolute\", left: at * F, top: 0 }}\n        >\n          {ch}\n        </span>\n      );\n    });\n  };\n\n  const numberBox = (style: React.CSSProperties, key: string) => (\n    <div\n      key={key}\n      style={{\n        position: \"absolute\",\n        left,\n        top: baseline - F,\n        width: textW + F * 0.1,\n        height: F * 1.3,\n        lineHeight: `${F * 1.2}px`,\n        fontFamily: hanken.fontFamily,\n        fontWeight: 300,\n        fontSize: F,\n        whiteSpace: \"pre\",\n        ...style,\n      }}\n    >\n      {glyphs(key)}\n    </div>\n  );\n\n  const labelP = interpolate(t, [F_ + 4, F_ + 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const cx = right;\n  const cy = baseline - capPx / 2;\n  const subject = `translate(${dx.toFixed(2)}px, 0px) translate(${cx}px, ${cy}px) scale(${ds.toFixed(4)}) translate(${-cx}px, ${-cy}px)`;\n  const dotR = smallFs * 0.32;\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: marginX,\n          top: marginY,\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: smallFs * 0.6,\n          fontFamily: hanken.fontFamily,\n          fontWeight: 500,\n          fontSize: smallFs,\n          lineHeight: 1,\n          whiteSpace: \"pre\",\n          color: lit ? c.ink : c.dim,\n        }}\n      >\n        <div\n          style={{\n            width: dotR * 2,\n            height: dotR * 2,\n            borderRadius: \"50%\",\n            background: dot,\n            opacity: lit ? 1 : 0.35 + pre * 0.65,\n            flexShrink: 0,\n          }}\n        />\n        {eyebrow}\n      </div>\n\n      <div style={{ position: \"absolute\", inset: 0, transform: subject }}>\n        {lit\n          ? numberBox(\n              {\n                transform: `translate(${shadowDx}px, ${shadowDy}px)`,\n                color: c.shadow,\n              },\n              \"s\",\n            )\n          : null}\n        {numberBox({ color: lit ? c.ink : c.dim }, \"n\")}\n      </div>\n\n      {label ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: marginX,\n            width: labelW,\n            ...(portrait\n              ? { top: baseline - capPx - smallFs * 4.2 }\n              : { bottom: H - baseline }),\n            fontFamily: hanken.fontFamily,\n            fontWeight: 400,\n            fontSize: smallFs,\n            lineHeight: 1.3,\n            color: c.muted,\n            textWrap: \"balance\",\n            opacity: labelP,\n          }}\n        >\n          {label}\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/flash-count.tsx"
    }
  ],
  "type": "registry:component"
}
