{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "beat-count",
  "title": "Beat Count",
  "description": "A big thin numeral counts to 64 in steps locked to a BPM, with a small caption.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/beat-count/index.tsx",
      "content": "\"use client\";\n\nimport { loadVariableFont } from \"@remotion/google-fonts/InterTight\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst inter = loadVariableFont(\"normal\", { subsets: [\"latin\"] });\n\nexport interface BeatCountProps {\n  value?: number;\n  from?: number;\n  beats?: number;\n  bpm?: number;\n  decimals?: 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 THEMES = {\n  dark: {\n    bg: \"#1C1C1E\",\n    ink: \"#E9E9E6\",\n    accent: \"#6D5BFF\",\n  },\n  light: {\n    bg: \"#E9E9E6\",\n    ink: \"#0B0B0C\",\n    accent: \"#6D5BFF\",\n  },\n} as const;\n\nconst WGHT = { rest: 200, duck: 500 } as const;\nconst TRACK = { rest: -0.045, duck: -0.01 } as const;\nconst DUCK = 0.2;\nconst LAST_DUCK = 0.3;\n\nconst FIRST_BEAT = 12;\nconst ENTER_LEN = 10;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nfunction format(v: number, decimals: number) {\n  return v.toLocaleString(\"en-US\", {\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n  });\n}\n\ninterface Metrics {\n  rest: number;\n  duck: number;\n  figTop: number;\n  baseline: number;\n}\n\nfunction measure(text: string): Metrics {\n  const ctx = document.createElement(\"canvas\").getContext(\"2d\");\n  let rest = 0.55 * text.length;\n  let duck = 0.62 * text.length;\n  let figTop = 0.72;\n  let baseline = 0.8;\n  if (ctx) {\n    ctx.font = `${WGHT.rest} 200px \"${inter.fontFamily}\"`;\n    ctx.letterSpacing = `${TRACK.rest * 200}px`;\n    rest = ctx.measureText(text).width / 200;\n    ctx.font = `${WGHT.duck} 200px \"${inter.fontFamily}\"`;\n    ctx.letterSpacing = `${TRACK.duck * 200}px`;\n    duck = ctx.measureText(text).width / 200;\n    const fm = ctx.measureText(\"0\");\n    const asc = fm.fontBoundingBoxAscent / 200;\n    const desc = fm.fontBoundingBoxDescent / 200;\n    baseline = (1 - (asc + desc)) / 2 + asc;\n    figTop = fm.actualBoundingBoxAscent / 200;\n  }\n  return { rest, duck, figTop, baseline };\n}\n\nfunction useMetrics(text: string) {\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"beat-count: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      inter.waitUntilDone(),\n      document.fonts.load(`${WGHT.rest} 100px \"${inter.fontFamily}\"`),\n      document.fonts.load(`${WGHT.duck} 100px \"${inter.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key: text, m: measure(text) });\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [text, handle]);\n  return state && state.key === text ? state.m : null;\n}\n\nfunction envelope(since: number, release: number) {\n  if (since < 0) return 0;\n  const attack = Math.min(1, (since + 1) / 2);\n  return attack * Math.exp(-Math.max(0, since - 1) / release);\n}\n\nexport function BeatCount({\n  value = 64,\n  from = 0,\n  beats = 8,\n  bpm = 120,\n  decimals = 0,\n  prefix = \"\",\n  suffix = \"\",\n  label = \"cuts, each one landing on the kick\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: BeatCountProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const spot = accent ?? c.accent;\n  const finalText = useMemo(\n    () => `${prefix}${format(value, decimals)}${suffix}`,\n    [prefix, value, decimals, suffix],\n  );\n  const m = useMetrics(finalText);\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 s = portrait ? W / 1080 : Math.min(W / 1280, H / 720);\n\n  const n = Math.max(1, Math.round(beats));\n  const beatLen = (fps * 60) / Math.max(30, bpm);\n  const lastBeat = FIRST_BEAT + (n - 1) * beatLen;\n  const k =\n    t < FIRST_BEAT\n      ? -1\n      : Math.min(n - 1, Math.floor((t - FIRST_BEAT) / beatLen));\n  const beatT = FIRST_BEAT + Math.max(0, k) * beatLen;\n  const isLast = k === n - 1;\n\n  const shown = k < 0 ? from : from + ((value - from) * (k + 1)) / n;\n  const text = `${prefix}${format(\n    Math.round(shown * 10 ** decimals) / 10 ** decimals,\n    decimals,\n  )}${suffix}`;\n\n  const release = Math.max(2, beatLen * 0.26);\n  let d = k < 0 ? 0 : envelope(t - beatT, release) * DUCK;\n  if (isLast) {\n    const since = t - lastBeat;\n    const back = spring({\n      frame: since - 2,\n      fps,\n      config: { damping: 9, stiffness: 120, mass: 0.8 },\n    });\n    const attack = Math.min(1, (since + 1) / 2);\n    d = LAST_DUCK * (since < 2 ? attack : 1 - back);\n  }\n  const duck = Math.max(0, Math.min(1, d / LAST_DUCK));\n  const sy = 1 - d;\n  const wght = WGHT.rest + (WGHT.duck - WGHT.rest) * duck;\n  const track = TRACK.rest + (TRACK.duck - TRACK.rest) * duck;\n\n  const margin = portrait ? W * 0.08 : W * 0.065;\n  const F = Math.min(\n    (W * (portrait ? 0.84 : 0.66)) / Math.max(0.1, m.duck),\n    (H * (portrait ? 0.3 : 0.66)) / Math.max(0.1, m.figTop),\n  );\n  const baseY = H - margin;\n  const boxTop = baseY - m.baseline * F;\n  const labelFs = (portrait ? 50 : 32) * s;\n\n  const enter = interpolate(t, [0, ENTER_LEN], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const labelP = interpolate(\n    t,\n    [FIRST_BEAT + beatLen, FIRST_BEAT + beatLen + 16],\n    [0, 1],\n    {\n      ...CLAMP,\n      easing: EASE_OUT,\n    },\n  );\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          right: margin - F * 0.02,\n          top: boxTop,\n          height: F,\n          lineHeight: `${F}px`,\n          whiteSpace: \"pre\",\n          fontFamily: inter.fontFamily,\n          fontSize: F,\n          fontVariationSettings: `\"wght\" ${wght.toFixed(1)}`,\n          fontVariantNumeric: \"tabular-nums\",\n          letterSpacing: `${track.toFixed(4)}em`,\n          color: c.ink,\n          opacity: enter,\n          transformOrigin: `100% ${(m.baseline * F).toFixed(1)}px`,\n          transform: `translateY(${((1 - enter) * F * 0.08).toFixed(2)}px) scaleY(${sy.toFixed(4)})`,\n        }}\n      >\n        {text}\n      </div>\n\n      {label ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: margin,\n            top: margin,\n            width: W * (portrait ? 0.7 : 0.34),\n            fontFamily: inter.fontFamily,\n            fontVariationSettings: `\"wght\" 400`,\n            fontSize: labelFs,\n            lineHeight: 1.2,\n            letterSpacing: \"-0.015em\",\n            textWrap: \"balance\",\n            color: spot,\n            opacity: labelP,\n            transform: `translateY(${((1 - labelP) * labelFs * 0.5).toFixed(2)}px)`,\n          }}\n        >\n          {label}\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/beat-count.tsx"
    }
  ],
  "type": "registry:component"
}
