{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "smash-count",
  "title": "Smash Count",
  "description": "A giant red 64 smash-cuts up from zero beside a small caption.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/smash-count/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadRubik } from \"@remotion/google-fonts/Rubik\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst rubik = loadRubik(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface SmashCountProps {\n  value?: number;\n  from?: number;\n  cuts?: number;\n  prefix?: string;\n  suffix?: string;\n  label?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst RED = \"#E60012\";\n\nconst THEMES = {\n  dark: { bg: \"#000000\", ink: \"#FFFFFF\" },\n  light: { bg: \"#FFFFFF\", ink: \"#000000\" },\n} as const;\n\ntype Anchor = \"c\" | \"tl\" | \"tr\" | \"bl\" | \"br\";\nconst ROUTE: Anchor[] = [\"c\", \"tr\", \"bl\", \"tl\", \"br\", \"c\", \"tr\", \"tl\"];\n\nconst TRACK = -0.04;\nconst FIRST_GAP = 13;\nconst LAST_GAP = 6;\nconst PUNCH_LEN = 5;\nconst LAND_LEN = 12;\n\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  widths: number[];\n  cap: number;\n  base: number;\n  label: number;\n}\n\nfunction cutValues(from: number, value: number, cuts: number) {\n  const n = Math.max(2, Math.min(cuts, Math.abs(value - from) + 1));\n  const out: number[] = [];\n  for (let k = 0; k < n; k++) {\n    const x = k / (n - 1);\n    const e = 1 - (1 - x) ** 1.7;\n    out.push(Math.round(from + (value - from) * e));\n  }\n  for (let k = 1; k < n; k++) {\n    if (out[k] === out[k - 1]) out[k] = out[k - 1] + Math.sign(value - from);\n  }\n  out[n - 1] = value;\n  return out;\n}\n\nfunction cutTimes(n: number) {\n  const out = [0];\n  for (let k = 1; k < n; k++) {\n    const g =\n      FIRST_GAP + (LAST_GAP - FIRST_GAP) * ((k - 1) / Math.max(1, n - 2));\n    out.push(out[k - 1] + g);\n  }\n  return out.map((v) => Math.round(v));\n}\n\nfunction measure(texts: string[], label: string): Metrics {\n  const ctx = document.createElement(\"canvas\").getContext(\"2d\");\n  if (!ctx) throw new Error(\"smash-count: no 2d context\");\n  ctx.font = `500 100px \"${rubik.fontFamily}\"`;\n  const widths = texts.map(\n    (s) =>\n      ctx.measureText(s).width / 100 +\n      TRACK * Math.max(0, Array.from(s).length - 1),\n  );\n  const fm = ctx.measureText(\"0123456789\");\n  const asc = fm.fontBoundingBoxAscent / 100;\n  const desc = fm.fontBoundingBoxDescent / 100;\n  ctx.font = `400 100px \"${rubik.fontFamily}\"`;\n  return {\n    widths,\n    cap: ctx.measureText(\"0\").actualBoundingBoxAscent / 100,\n    base: (1 - (asc + desc)) / 2 + asc,\n    label: label ? ctx.measureText(label).width / 100 : 0,\n  };\n}\n\nfunction useMetrics(texts: string[], label: string) {\n  const key = [...texts, label].join(\"\\u0001\");\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"smash-count: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    const parts = key.split(\"\\u0001\");\n    Promise.all([\n      rubik.waitUntilDone(),\n      document.fonts.load(`500 100px \"${rubik.fontFamily}\"`),\n      document.fonts.load(`400 100px \"${rubik.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({\n          key,\n          m: measure(parts.slice(0, -1), parts[parts.length - 1] ?? \"\"),\n        });\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 splitLabel(label: string) {\n  const words = label.split(\" \").filter(Boolean);\n  if (words.length < 3) return [label];\n  let best = 1;\n  let bestDiff = Number.POSITIVE_INFINITY;\n  for (let i = 1; i < words.length; i++) {\n    const a = words.slice(0, i).join(\" \").length;\n    const b = words.slice(i).join(\" \").length;\n    const d = Math.abs(a - b);\n    if (d < bestDiff) {\n      bestDiff = d;\n      best = i;\n    }\n  }\n  return [words.slice(0, best).join(\" \"), words.slice(best).join(\" \")];\n}\n\nexport function SmashCount({\n  value = 64,\n  from = 1,\n  cuts = 6,\n  prefix = \"\",\n  suffix = \"\",\n  label = \"scenes, zero keyframes\",\n  accent = RED,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: SmashCountProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const values = useMemo(\n    () => cutValues(Math.round(from), Math.round(value), Math.round(cuts)),\n    [from, value, cuts],\n  );\n  const texts = useMemo(\n    () => values.map((v) => `${prefix}${v.toLocaleString(\"en-US\")}${suffix}`),\n    [values, prefix, suffix],\n  );\n  const lines = useMemo(() => (label ? splitLabel(label) : []), [label]);\n  const m = useMetrics(\n    texts,\n    lines.reduce((a, b) => (a.length > b.length ? a : b), \"\"),\n  );\n\n  const transparent = background === \"transparent\";\n  const stage = transparent ? \"transparent\" : (background ?? c.bg);\n\n  if (!m) {\n    return (\n      <div style={{ position: \"absolute\", inset: 0, background: stage }} />\n    );\n  }\n\n  const portrait = H > W * 1.1;\n  const n = texts.length;\n  const times = cutTimes(n);\n  let k = 0;\n  for (let i = 0; i < n; i++) if (t >= times[i]) k = i;\n  const last = k === n - 1;\n  const local = t - times[k];\n\n  const margin = Math.min(W, H) * 0.07;\n  const anchor: Anchor = last ? \"bl\" : ROUTE[k % ROUTE.length];\n  const x = k / Math.max(1, n - 1);\n  const capFrac = last\n    ? portrait\n      ? 0.34\n      : 0.74\n    : (portrait ? 0.12 : 0.24) + (portrait ? 0.14 : 0.3) * x ** 1.2;\n  const F = Math.min(\n    (capFrac * H) / m.cap,\n    (W - 2 * margin) / Math.max(0.2, m.widths[k]),\n  );\n  const textW = m.widths[k] * F;\n  const capPx = m.cap * F;\n\n  const left =\n    anchor === \"c\"\n      ? (W - textW) / 2\n      : anchor === \"tl\" || anchor === \"bl\"\n        ? margin\n        : W - margin - textW;\n  const baseY =\n    anchor === \"c\"\n      ? (H + capPx) / 2\n      : anchor === \"tl\" || anchor === \"tr\"\n        ? margin + capPx\n        : H - margin;\n\n  const punch = last\n    ? interpolate(local, [0, LAND_LEN], [1.05, 1], {\n        ...CLAMP,\n        easing: EASE_OUT,\n      })\n    : interpolate(local, [0, PUNCH_LEN], [1.08, 1], {\n        ...CLAMP,\n        easing: EASE_OUT,\n      });\n  const ox =\n    anchor === \"c\" ? \"50%\" : anchor === \"tl\" || anchor === \"bl\" ? \"0%\" : \"100%\";\n  const oy = `${(m.base * 100).toFixed(1)}%`;\n\n  const labelFs = Math.min(\n    H * (portrait ? 0.03 : 0.052),\n    (W * (portrait ? 0.8 : 0.42)) / Math.max(0.1, m.label),\n  );\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left,\n          top: baseY - m.base * F,\n          height: F,\n          lineHeight: `${F}px`,\n          whiteSpace: \"pre\",\n          fontFamily: rubik.fontFamily,\n          fontWeight: 500,\n          fontSize: F,\n          letterSpacing: `${TRACK}em`,\n          color: accent,\n          transform: `scale(${punch.toFixed(4)})`,\n          transformOrigin: `${ox} ${oy}`,\n        }}\n      >\n        {texts[k]}\n      </div>\n      {last && lines.length > 0 ? (\n        <div\n          style={{\n            position: \"absolute\",\n            right: margin,\n            top: margin - labelFs * 0.1,\n            textAlign: \"right\",\n            fontFamily: rubik.fontFamily,\n            fontWeight: 400,\n            fontSize: labelFs,\n            lineHeight: 1.12,\n            letterSpacing: \"-0.015em\",\n            whiteSpace: \"pre\",\n            color: c.ink,\n          }}\n        >\n          {lines.join(\"\\n\")}\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/smash-count.tsx"
    }
  ],
  "type": "registry:component"
}
