{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-wheel",
  "title": "Number Wheel",
  "description": "Mechanical odometer that rolls an integer up or down to its target, direction inferred from the values.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/number-wheel/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadMono } from \"@remotion/google-fonts/JetBrainsMono\";\nimport type React from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: MONO_FAMILY } = loadMono();\n\nexport interface NumberWheelProps {\n  from?: number;\n  to?: number;\n  fontSize?: number;\n  color?: string;\n  speed?: number;\n}\n\nexport interface OdometerProps {\n  current: number;\n  fontSize: number;\n  color: string;\n}\n\nconst WHEEL_ROLL_START = 0.9;\nconst SPRING_FRAMES = 30;\nconst SPRING_CONFIG = { damping: 200, stiffness: 100, mass: 1 } as const;\nconst COUNT_PORTION = 0.8;\n\nfunction rollFraction(current: number, place: number): number {\n  const v = current / 10 ** place;\n  const frac = v - Math.floor(v);\n  if (frac <= WHEEL_ROLL_START) return 0;\n  return (frac - WHEEL_ROLL_START) / (1 - WHEEL_ROLL_START);\n}\n\nfunction computeWheel(\n  current: number,\n  place: number,\n  springEased: (t: number) => number,\n): number {\n  if (current <= 0) return 0;\n  if (place === 0) return current % 10;\n\n  const digit = Math.floor(current / 10 ** place) % 10;\n  const t = rollFraction(current, place);\n  if (t <= 0) return digit;\n  return digit + springEased(t);\n}\n\nfunction placeOpacity(current: number, place: number): number {\n  if (place === 0) return 1;\n  const threshold = 10 ** place;\n  return interpolate(current, [threshold * 0.9, threshold], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n}\n\nfunction DigitColumn({\n  value,\n  fontSize,\n  opacity,\n}: {\n  value: number;\n  fontSize: number;\n  opacity: number;\n}) {\n  const cellHeight = fontSize * 1.1;\n  return (\n    <span\n      style={{\n        display: \"inline-block\",\n        overflow: \"hidden\",\n        height: cellHeight,\n        width: \"0.62em\",\n        verticalAlign: \"top\",\n        textAlign: \"center\",\n        opacity,\n      }}\n    >\n      <span\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          translate: `0 ${-value * cellHeight}px`,\n        }}\n      >\n        {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0].map((d, i) => (\n          <span\n            key={i}\n            style={{ height: cellHeight, lineHeight: `${cellHeight}px` }}\n          >\n            {d}\n          </span>\n        ))}\n      </span>\n    </span>\n  );\n}\n\nfunction OdometerColumns({\n  current,\n  templateValue,\n  fontSize,\n  color,\n  fps,\n}: {\n  current: number;\n  templateValue: number;\n  fontSize: number;\n  color: string;\n  fps: number;\n}) {\n  const springAtEnd = spring({\n    frame: SPRING_FRAMES,\n    fps,\n    config: SPRING_CONFIG,\n  });\n  const springEased = (t: number) =>\n    spring({ frame: t * SPRING_FRAMES, fps, config: SPRING_CONFIG }) /\n    springAtEnd;\n\n  const template = templateValue.toLocaleString(\"en-US\");\n\n  const cells: React.ReactNode[] = [];\n  let place = 0;\n  for (let i = template.length - 1; i >= 0; i--) {\n    const ch = template[i];\n    if (ch === \",\") {\n      cells.unshift(\n        <span\n          key={`c${i}`}\n          style={{\n            display: \"inline-block\",\n            width: \"0.32em\",\n            color: \"currentColor\",\n            fontSize: \"0.85em\",\n            textAlign: \"center\",\n            opacity: placeOpacity(current, place),\n          }}\n        >\n          ,\n        </span>,\n      );\n    } else {\n      const wheel = computeWheel(current, place, springEased);\n      cells.unshift(\n        <DigitColumn\n          key={`d${i}`}\n          value={wheel}\n          fontSize={fontSize}\n          opacity={placeOpacity(current, place)}\n        />,\n      );\n      place++;\n    }\n  }\n\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"baseline\",\n        fontSize,\n        fontWeight: 800,\n        color,\n        fontFamily: MONO_FAMILY,\n        fontVariantNumeric: \"tabular-nums\",\n        letterSpacing: \"-0.03em\",\n        lineHeight: 1,\n      }}\n    >\n      {cells}\n    </div>\n  );\n}\n\nexport function Odometer({ current, fontSize, color }: OdometerProps) {\n  const { fps } = useVideoConfig();\n  const value = Math.max(0, Math.round(current));\n  return (\n    <OdometerColumns\n      current={current}\n      templateValue={value}\n      fontSize={fontSize}\n      color={color}\n      fps={fps}\n    />\n  );\n}\n\nexport function NumberWheel({\n  from = 0,\n  to = 24813,\n  fontSize = 120,\n  color = \"#171717\",\n  speed = 1,\n}: NumberWheelProps) {\n  const frame = useCurrentFrame();\n  const { durationInFrames, fps } = useVideoConfig();\n\n  const start = Math.max(0, Math.round(from));\n  const end = Math.max(0, Math.round(to));\n\n  const progress = interpolate(\n    frame * speed,\n    [0, durationInFrames * COUNT_PORTION],\n    [0, 1],\n    {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: Easing.bezier(0.5, 1, 0.5, 1),\n    },\n  );\n  const current = Math.max(0, start + progress * (end - start));\n  const maxVal = Math.max(start, end);\n\n  return (\n    <AbsoluteFill\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <OdometerColumns\n        current={current}\n        templateValue={maxVal}\n        fontSize={fontSize}\n        color={color}\n        fps={fps}\n      />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/number-wheel.tsx"
    }
  ],
  "type": "registry:component"
}
