{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "packed-count",
  "title": "Packed Count",
  "description": "A big blue counter reaches 41,900 with digits packing tight, caption above.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/packed-count/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadFigtree } from \"@remotion/google-fonts/Figtree\";\nimport type React from \"react\";\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 figtree = loadFigtree(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface PackedCountProps {\n  value?: number;\n  from?: 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 SKY = \"#1185FE\";\n\nconst THEMES = {\n  dark: { bg: \"#0B0F14\", ink: \"#F5F8FA\" },\n  light: { bg: \"#FFFFFF\", ink: \"#0B0F14\" },\n} as const;\nconst TIGHT = -0.03;\nconst FONT_PX = 400;\n\nconst RISE_AT = 0;\nconst RISE_LEN = 16;\nconst RISE_GAP = 3;\nconst LABEL_AT = 10;\nconst COUNT_AT = 18;\nconst COUNT_LEN = 64;\nconst FILL_AT = 80;\nconst FILL_LEN = 20;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_COUNT = Easing.bezier(0.42, 0, 0.3, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface Slot {\n  ch: string;\n  index: number;\n  kind: \"digit\" | \"sep\" | \"fix\";\n  place: number;\n}\n\nfunction group(n: number) {\n  const s = String(Math.max(0, Math.floor(n)));\n  let out = \"\";\n  for (let i = 0; i < s.length; i++) {\n    const fromEnd = s.length - i;\n    if (i > 0 && fromEnd % 3 === 0) out += \",\";\n    out += s[i];\n  }\n  return out;\n}\n\nfunction buildSlots(value: number, prefix: string, suffix: string): Slot[] {\n  const body = Array.from(group(value));\n  const slots: Slot[] = [];\n  Array.from(prefix).forEach((ch) => {\n    slots.push({ ch, index: slots.length, kind: \"fix\", place: -1 });\n  });\n  let digitsRight = body.filter((c) => c !== \",\").length;\n  for (const ch of body) {\n    if (ch === \",\") {\n      slots.push({ ch, index: slots.length, kind: \"sep\", place: digitsRight });\n    } else {\n      digitsRight--;\n      slots.push({\n        ch,\n        index: slots.length,\n        kind: \"digit\",\n        place: digitsRight,\n      });\n    }\n  }\n  Array.from(suffix).forEach((ch) => {\n    slots.push({ ch, index: slots.length, kind: \"fix\", place: -1 });\n  });\n  return slots;\n}\n\ninterface Metrics {\n  cell: number;\n  adv: Record<string, number>;\n  cap: number;\n  asc: number;\n  desc: number;\n  label: number;\n}\n\nfunction measure(slots: Slot[], label: string): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) throw new Error(\"packed-count: no 2d context\");\n  ctx.font = `500 ${FONT_PX}px \"${figtree.fontFamily}\"`;\n  let cell = 0;\n  for (const d of \"0123456789\") {\n    cell = Math.max(cell, ctx.measureText(d).width / FONT_PX);\n  }\n  const adv: Record<string, number> = {};\n  for (const s of slots) {\n    if (s.kind !== \"digit\") adv[s.ch] = ctx.measureText(s.ch).width / FONT_PX;\n  }\n  const zm = ctx.measureText(\"0\");\n  const cap = zm.actualBoundingBoxAscent / FONT_PX;\n  const asc = zm.fontBoundingBoxAscent / FONT_PX;\n  const desc = zm.fontBoundingBoxDescent / FONT_PX;\n  return {\n    cell,\n    adv,\n    cap,\n    asc,\n    desc,\n    label: label ? ctx.measureText(label).width / FONT_PX : 0,\n  };\n}\n\nfunction useMetrics(slots: Slot[], label: string) {\n  const [metrics, setMetrics] = useState<Metrics | null>(null);\n  const [handle] = useState(() => delayRender(\"packed-count: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      figtree.waitUntilDone(),\n      document.fonts.load(`500 100px \"${figtree.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setMetrics(measure(slots, label));\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [slots, label, handle]);\n  return metrics;\n}\n\nexport function PackedCount({\n  value = 41900,\n  from = 120,\n  prefix = \"\",\n  suffix = \"\",\n  label = \"replays on a single launch clip\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: PackedCountProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const bg = background ?? c.bg;\n  const sky = accent ?? SKY;\n\n  const target = Math.max(1, Math.floor(Math.abs(value)));\n  const start = Math.min(target, Math.max(1, Math.floor(Math.abs(from))));\n  const slots = useMemo(\n    () => buildSlots(target, prefix, suffix),\n    [target, prefix, suffix],\n  );\n  const m = useMetrics(slots, label);\n\n  const logA = Math.log(start);\n  const logB = Math.log(target);\n  const countAt = (frame: number) => {\n    const p = interpolate(frame, [COUNT_AT, COUNT_AT + COUNT_LEN], [0, 1], {\n      ...CLAMP,\n      easing: EASE_COUNT,\n    });\n    if (p >= 1) return target;\n    const v = Math.floor(Math.exp(logA + (logB - logA) * p) + 1e-6);\n    return Math.min(target, Math.max(start, v));\n  };\n  const joinAt = useMemo(() => {\n    const startDigits = String(start).length;\n    const out: number[] = [];\n    const maxPlace = String(target).length;\n    for (let place = 0; place < maxPlace; place++) {\n      if (place < startDigits) {\n        out.push(Number.NEGATIVE_INFINITY);\n        continue;\n      }\n      const need = 10 ** place;\n      let f = COUNT_AT;\n      while (f < COUNT_AT + COUNT_LEN) {\n        const p = interpolate(f, [COUNT_AT, COUNT_AT + COUNT_LEN], [0, 1], {\n          ...CLAMP,\n          easing: EASE_COUNT,\n        });\n        if (Math.exp(logA + (logB - logA) * p) >= need) break;\n        f += 0.25;\n      }\n      out.push(f);\n    }\n    return out;\n  }, [start, target, logA, logB]);\n\n  if (!m) {\n    return <div style={{ position: \"absolute\", inset: 0, background: bg }} />;\n  }\n\n  const margin = Math.min(W, H) * 0.065;\n  const advOf = (s: Slot) => (s.kind === \"digit\" ? m.cell : m.adv[s.ch]);\n  const finalSum = slots.reduce((a, s) => a + advOf(s), 0);\n  const finalSpan = finalSum + TIGHT * (slots.length - 1);\n  const F = Math.min(H * 0.56, (W - margin * 2) / finalSpan);\n  const span = finalSpan * F;\n  const left = margin;\n\n  const presence = slots.map((s) => {\n    if (s.kind === \"fix\") return 1;\n    const at = joinAt[s.place] + (s.kind === \"sep\" ? 3 : 0);\n    if (!Number.isFinite(at)) return 1;\n    return spring({\n      frame: t - at,\n      fps,\n      config: {\n        damping: 22,\n        stiffness: 150,\n        mass: 0.8,\n        overshootClamping: true,\n      },\n    });\n  });\n\n  let sumAdv = 0;\n  let count = 0;\n  slots.forEach((s, i) => {\n    sumAdv += presence[i] * advOf(s) * F;\n    count += presence[i];\n  });\n  const gap = count > 1 ? (span - sumAdv) / (count - 1) : 0;\n\n  const xs: number[] = [];\n  let cursor = left;\n  slots.forEach((s, i) => {\n    const p = presence[i];\n    const a = advOf(s) * F;\n    xs.push(cursor - (1 - p) * a);\n    cursor += p * (a + gap);\n  });\n\n  const shown = group(countAt(t));\n  const shownDigits = Array.from(shown).filter((ch) => ch !== \",\");\n  const digitSlots = slots.filter((s) => s.kind === \"digit\");\n\n  const baseY = H - margin - m.desc * F * 0.35;\n  const lineTop = baseY - (m.asc + (1 - m.asc - m.desc) / 2) * F;\n  const maskTop = baseY - m.cap * F - F * 0.12;\n  const maskH = m.cap * F + F * 0.3;\n\n  const labelSize = label\n    ? Math.min(H * 0.064, W * 0.06, ((W - margin * 2) * 0.9) / m.label)\n    : 0;\n\n  const fillStep = slots.length > 1 ? 10 / (slots.length - 1) : 0;\n  const firstPresent = slots.findIndex((_, i) => presence[i] > 0.999);\n  const glyphs: React.ReactNode[] = [];\n  slots.forEach((s, i) => {\n    const p = presence[i];\n    if (p <= 0.001) return;\n    let ch = s.ch;\n    if (s.kind === \"digit\") {\n      const k = digitSlots.length - 1 - s.place;\n      const fromRight = shownDigits.length - 1 - s.place;\n      ch = fromRight >= 0 ? shownDigits[fromRight] : digitSlots[k].ch;\n    }\n    const opening = s.kind === \"fix\" || !Number.isFinite(joinAt[s.place]);\n    const order = Math.max(0, i - Math.max(0, firstPresent));\n    const intro = opening\n      ? interpolate(\n          t,\n          [RISE_AT + order * RISE_GAP, RISE_AT + order * RISE_GAP + RISE_LEN],\n          [0, 1],\n          { ...CLAMP, easing: EASE_OUT },\n        )\n      : 1;\n    const opacity = interpolate(p, [0, 0.55], [0, 1], CLAMP);\n    const fillP = interpolate(\n      t,\n      [FILL_AT + i * fillStep, FILL_AT + i * fillStep + FILL_LEN],\n      [0, 1],\n      { ...CLAMP, easing: EASE_OUT },\n    );\n    const w = advOf(s) * F;\n    const style: React.CSSProperties = {\n      position: \"absolute\",\n      left: xs[i],\n      top: lineTop - maskTop + (1 - intro) * maskH * 1.05,\n      width: w,\n      textAlign: \"center\",\n      fontFamily: figtree.fontFamily,\n      fontWeight: 500,\n      fontSize: F,\n      height: F * 1.3,\n      lineHeight: `${F}px`,\n      whiteSpace: \"pre\",\n      opacity,\n      color: c.ink,\n    };\n    glyphs.push(\n      <div key={`s${s.index}`} style={style}>\n        {ch}\n      </div>,\n    );\n    if (fillP > 0) {\n      glyphs.push(\n        <div\n          key={`f${s.index}`}\n          style={{ ...style, opacity: opacity * fillP, color: sky }}\n        >\n          {ch}\n        </div>,\n      );\n    }\n  });\n\n  const labelP = interpolate(t, [LABEL_AT, LABEL_AT + 18], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  return (\n    <AbsoluteFill style={{ background: bg, overflow: \"hidden\" }}>\n      {label ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: margin,\n            top: margin - labelSize * 0.1,\n            height: labelSize * 1.3,\n            overflow: \"hidden\",\n            fontFamily: figtree.fontFamily,\n            fontWeight: 500,\n            fontSize: labelSize,\n            lineHeight: `${labelSize * 1.2}px`,\n            letterSpacing: \"-0.015em\",\n            color: c.ink,\n            whiteSpace: \"pre\",\n          }}\n        >\n          <div\n            style={{\n              transform: `translateY(${((1 - labelP) * labelSize * 1.3).toFixed(2)}px)`,\n            }}\n          >\n            {label}\n          </div>\n        </div>\n      ) : null}\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          width: W,\n          top: maskTop,\n          height: maskH,\n          overflow: \"hidden\",\n        }}\n      >\n        {glyphs}\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/packed-count.tsx"
    }
  ],
  "type": "registry:component"
}
