{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carry-count",
  "title": "Carry Count",
  "description": "A big counter reaches 13,580 with a small caption, \"scenes students built in week one\".",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/carry-count/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Nunito\";\nimport type React from \"react\";\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 nunito = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface CarryCountProps {\n  value?: number;\n  from?: number;\n  steps?: number;\n  label?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: {\n    bg: \"#133A24\",\n    ink: \"#EAF7EC\",\n    accent: \"#FFD23F\",\n    carryInk: \"#133A24\",\n  },\n  light: {\n    bg: \"#FFD23F\",\n    ink: \"#133A24\",\n    accent: \"#FFFFFF\",\n    carryInk: \"#133A24\",\n  },\n} as const;\n\nconst FONT_PX = 200;\nconst TRACK = -0.02;\nconst LABEL_TRACK = -0.01;\nconst START = 16;\nconst STEP_LENS = [26, 20, 16, 14, 13, 12, 12, 12];\nconst COL_GAP = 5;\nconst NEW_COL_GAP = 3;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IN = Easing.bezier(0.5, 0, 0.9, 0.4);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface Metrics {\n  slot: number;\n  digit: number[];\n  comma: number;\n  cap: number;\n  labelW: number[];\n  labelCap: number;\n  labelDesc: number;\n  asc: number;\n  desc: number;\n}\n\nfunction measure(lines: string[]): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) throw new Error(\"carry-count: no 2d context\");\n  const fam = `\"${nunito.fontFamily}\"`;\n  ctx.font = `500 ${FONT_PX}px ${fam}`;\n  const digit: number[] = [];\n  for (let d = 0; d < 10; d++) {\n    digit.push(ctx.measureText(String(d)).width / FONT_PX);\n  }\n  const slot = Math.max(...digit) + TRACK;\n  const comma = ctx.measureText(\",\").width / FONT_PX + TRACK;\n  const cap = ctx.measureText(\"0\").actualBoundingBoxAscent / FONT_PX;\n  ctx.font = `500 ${FONT_PX}px ${fam}`;\n  const labelW = lines.map(\n    (l) =>\n      ctx.measureText(l).width / FONT_PX +\n      LABEL_TRACK * Math.max(0, l.length - 1),\n  );\n  const lm = ctx.measureText(\"Hg\");\n  return {\n    asc: lm.fontBoundingBoxAscent / FONT_PX,\n    desc: lm.fontBoundingBoxDescent / FONT_PX,\n    slot,\n    digit,\n    comma,\n    cap,\n    labelW,\n    labelCap: ctx.measureText(\"H\").actualBoundingBoxAscent / FONT_PX,\n    labelDesc: lm.actualBoundingBoxDescent / FONT_PX,\n  };\n}\n\nfunction useMetrics(lines: string[]) {\n  const key = lines.join(\"\\n\");\n  const [metrics, setMetrics] = useState<Metrics | null>(null);\n  const [handle] = useState(() => delayRender(\"carry-count: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      nunito.waitUntilDone(),\n      document.fonts.load(`500 100px \"${nunito.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setMetrics(measure(key.split(\"\\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 metrics;\n}\n\nfunction digitsOf(n: number) {\n  return Array.from(String(Math.max(0, Math.round(n))))\n    .reverse()\n    .map(Number);\n}\n\ninterface Change {\n  col: number;\n  at: number;\n  digit: number;\n}\n\ninterface Carry {\n  col: number;\n  at: number;\n  land: number;\n}\n\nfunction plan(from: number, value: number, steps: number) {\n  const n = Math.max(1, Math.round(steps));\n  const changes: Change[] = [];\n  const carries: Carry[] = [];\n  let cur = digitsOf(from);\n  let at = START;\n  for (let s = 0; s < n; s++) {\n    const target = Math.round(from + ((value - from) * (s + 1)) / n);\n    const prev = Math.round(from + ((value - from) * s) / n);\n    const add = digitsOf(Math.max(0, target - prev));\n    const next: number[] = [];\n    let carry = 0;\n    let time = at;\n    const len = Math.max(cur.length, add.length);\n    for (let i = 0; i < len || carry > 0; i++) {\n      const sum = (cur[i] ?? 0) + (add[i] ?? 0) + carry;\n      const d = sum % 10;\n      next.push(d);\n      if (d !== cur[i] || i >= cur.length) {\n        changes.push({ col: i, at: time, digit: d });\n      }\n      carry = sum >= 10 ? 1 : 0;\n      const gap = i + 1 >= len ? NEW_COL_GAP : COL_GAP;\n      if (carry) carries.push({ col: i, at: time, land: time + gap });\n      time += gap;\n    }\n    cur = next;\n    at += STEP_LENS[Math.min(s, STEP_LENS.length - 1)];\n  }\n  const end = changes.reduce((m, c) => Math.max(m, c.at), START);\n  return { start: digitsOf(from), changes, carries, cols: cur.length, end };\n}\n\nfunction impact(u: number) {\n  if (u < 0) return 0;\n  return Math.exp(-u / 3.2) * Math.cos(u * 0.62);\n}\n\nexport function CarryCount({\n  value = 13580,\n  from = 2855,\n  steps = 5,\n  label = \"scenes students\\nbuilt in week one\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: CarryCountProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const bg = background ?? c.bg;\n  const disc = accent ?? c.accent;\n  const lines = useMemo(\n    () =>\n      label\n        .split(\"\\n\")\n        .map((l) => l.trim())\n        .filter(Boolean),\n    [label],\n  );\n  const m = useMetrics(lines);\n  const p = useMemo(\n    () => plan(Math.min(from, value), Math.max(from, value), steps),\n    [from, value, steps],\n  );\n\n  if (!m) {\n    return <div style={{ position: \"absolute\", inset: 0, background: bg }} />;\n  }\n\n  const cols = p.cols;\n  const commaAfter = (i: number) => i > 0 && i % 3 === 0;\n  const offsets: number[] = [];\n  let run = 0;\n  for (let i = 0; i < cols; i++) {\n    if (commaAfter(i)) run += m.comma;\n    run += m.slot;\n    offsets.push(run);\n  }\n  const numW = run;\n  const labelW = Math.max(0, ...m.labelW);\n  const portrait = H > W;\n  const LR = 0.25;\n  const lead = 1.12;\n  const labelBlock =\n    lines.length > 0 ? (lines.length - 1) * lead + m.labelCap : 0;\n\n  let F: number;\n  let numLeft: number;\n  let base: number;\n  let labelLeft: number;\n  let labelBase: number;\n  if (portrait) {\n    F = Math.min((W * 0.86) / numW, H * 0.2);\n    const L = F * LR * 1.25;\n    const gapV = F * 0.34;\n    const blockH = F * m.cap * 1.5 + gapV + labelBlock * L;\n    const top = (H - blockH) / 2 + F * m.cap * 0.5;\n    base = top + F * m.cap;\n    numLeft = (W - numW * F) / 2;\n    labelLeft = numLeft;\n    labelBase = base + gapV + m.labelCap * L;\n  } else {\n    const gapH = 0.12;\n    F = Math.min((W * 0.86) / (numW + gapH + labelW * LR), H * 0.4);\n    const total = (numW + (lines.length ? gapH + labelW * LR : 0)) * F;\n    numLeft = (W - total) / 2;\n    base = H / 2 + (F * m.cap * 1.42) / 2;\n    labelLeft = numLeft + (numW + gapH) * F;\n    labelBase = base - (lines.length - 1) * lead * F * LR;\n  }\n  const L = portrait ? F * LR * 1.25 : F * LR;\n  const right = numLeft + numW * F;\n  const colX = (i: number) => right - offsets[i] * F;\n  const capTop = base - m.cap * F;\n  const bo = (size: number, lh: number) =>\n    (lh - (m.asc + m.desc) * size) / 2 + m.asc * size;\n  const digitTop = base - bo(F, F);\n\n  const digitAt = (i: number) => {\n    let d: number | null = i < p.start.length ? p.start[i] : null;\n    let last = -1e9;\n    for (const ch of p.changes) {\n      if (ch.col === i && ch.at <= t) {\n        d = ch.digit;\n        last = ch.at;\n      }\n    }\n    return { d, last };\n  };\n\n  const intro = interpolate(t, [0, 18], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  const glyphs: React.ReactNode[] = [];\n  for (let i = 0; i < cols; i++) {\n    const { d, last } = digitAt(i);\n    if (d === null) continue;\n    const born = i >= p.start.length ? last : -1e9;\n    const grow =\n      born > -1e8\n        ? interpolate(t - born, [0, 7], [0.55, 1], {\n            ...CLAMP,\n            easing: EASE_OUT,\n          })\n        : 1;\n    const q = impact(t - last);\n    const sy = (1 - 0.16 * q) * grow;\n    const sx = (1 + 0.1 * q) * grow;\n    const x = colX(i);\n    const w = m.digit[d] * F;\n    glyphs.push(\n      <div\n        key={`d${i}`}\n        style={{\n          position: \"absolute\",\n          left: x + (m.slot * F - w) / 2,\n          top: digitTop,\n          height: F,\n          fontSize: F,\n          lineHeight: `${F}px`,\n          fontFamily: nunito.fontFamily,\n          fontWeight: 500,\n          color: c.ink,\n          whiteSpace: \"pre\",\n          transform: `scale(${sx.toFixed(4)}, ${sy.toFixed(4)})`,\n          transformOrigin: `50% ${bo(F, F).toFixed(1)}px`,\n          opacity: intro,\n        }}\n      >\n        {d}\n      </div>,\n    );\n    if (commaAfter(i) && i < cols) {\n      const cx = colX(i) + m.slot * F;\n      glyphs.push(\n        <div\n          key={`c${i}`}\n          style={{\n            position: \"absolute\",\n            left: cx,\n            top: digitTop,\n            height: F,\n            fontSize: F,\n            lineHeight: `${F}px`,\n            fontFamily: nunito.fontFamily,\n            fontWeight: 500,\n            color: c.ink,\n            whiteSpace: \"pre\",\n            opacity: intro * grow,\n          }}\n        >\n          ,\n        </div>,\n      );\n    }\n  }\n\n  const D = F * 0.34;\n  const carryNodes: React.ReactNode[] = [];\n  for (const k of p.carries) {\n    const span = k.land - k.at;\n    const u = ((t - k.at) / span) * COL_GAP;\n    if (u < 0 || u > COL_GAP + 1) continue;\n    const x0 = colX(k.col) + (m.slot * F) / 2;\n    const x1 = colX(k.col + 1) + (m.slot * F) / 2;\n    const hoverY = capTop - D * 0.95;\n    const startY = capTop + D * 0.2;\n    const a = interpolate(u, [0, COL_GAP * 0.52], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n    const dropP = interpolate(u, [COL_GAP * 0.68, COL_GAP], [0, 1], {\n      ...CLAMP,\n      easing: EASE_IN,\n    });\n    const x = x0 + (x1 - x0) * a;\n    const lift = Math.sin(a * Math.PI) * D * 0.55;\n    const y =\n      startY +\n      (hoverY - startY) * a -\n      lift +\n      (capTop + D * 0.15 - hoverY) * dropP;\n    const s =\n      interpolate(u, [0, 1.6], [0.5, 1], { ...CLAMP, easing: EASE_OUT }) *\n      (1 - 0.35 * dropP);\n    const o = interpolate(u, [COL_GAP - 0.4, COL_GAP + 0.6], [1, 0], CLAMP);\n    carryNodes.push(\n      <div\n        key={`k${k.col}-${k.at}`}\n        style={{\n          position: \"absolute\",\n          left: x - D / 2,\n          top: y - D / 2,\n          width: D,\n          height: D,\n          borderRadius: D,\n          background: disc,\n          color: c.carryInk,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontFamily: nunito.fontFamily,\n          fontWeight: 500,\n          fontSize: D * 0.72,\n          lineHeight: 1,\n          transform: `scale(${s.toFixed(3)})`,\n          opacity: o,\n        }}\n      >\n        <span style={{ transform: `translateY(${(-D * 0.02).toFixed(2)}px)` }}>\n          1\n        </span>\n      </div>,\n    );\n  }\n\n  const labelNodes = lines.map((line, k) => {\n    const lp = interpolate(t, [6 + k * 4, 26 + k * 4], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n    const yb = labelBase + k * lead * L;\n    return (\n      <div\n        key={`l${k}`}\n        style={{\n          position: \"absolute\",\n          left: labelLeft,\n          top: yb - bo(L, L * 1.45),\n          height: L * 1.45,\n          overflow: \"hidden\",\n        }}\n      >\n        <div\n          style={{\n            fontFamily: nunito.fontFamily,\n            fontWeight: 500,\n            fontSize: L,\n            lineHeight: `${L * 1.45}px`,\n            letterSpacing: `${LABEL_TRACK}em`,\n            color: c.ink,\n            whiteSpace: \"pre\",\n            transform: `translateY(${((1 - lp) * L * 1.3).toFixed(2)}px)`,\n          }}\n        >\n          {line}\n        </div>\n      </div>\n    );\n  });\n\n  return (\n    <AbsoluteFill style={{ background: bg, overflow: \"hidden\" }}>\n      {glyphs}\n      {carryNodes}\n      {labelNodes}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/carry-count.tsx"
    }
  ],
  "type": "registry:component"
}
