{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-roll",
  "type": "registry:ui",
  "title": "Number roll",
  "description": "An odometer style animated number where each digit rolls on a spring and only changed digits move. Supports Intl formatting, trend tint and reduced motion.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/number-roll.tsx",
      "type": "registry:ui",
      "target": "components/ui/number-roll.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\n/*\n * NumberRoll: an odometer. Each digit is a strip of 0-9 that springs to its\n * place, so changing $1,284 to $1,391 rolls only the digits that changed.\n * Digits keep their identity from the right, so 999 → 1,000 grows a column\n * instead of reshuffling. Screen readers get the plain formatted value.\n */\n\ntype NumberRollProps = {\n  value?: number\n  /** Intl.NumberFormat options, e.g. { style: \"currency\", currency: \"USD\" }. */\n  format?: Intl.NumberFormatOptions\n  locale?: string\n  /** Briefly tint up/down changes with success/danger. */\n  trend?: boolean\n  className?: string\n}\n\nconst DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\nconst SPRING = { type: \"spring\", stiffness: 260, damping: 28, mass: 0.9 } as const\n\nfunction Digit({ d, reduce }: { d: number; reduce: boolean | null }) {\n  return (\n    <span className=\"relative -my-[0.14em] inline-block py-[0.14em] [mask-image:linear-gradient(transparent,black_0.16em,black_calc(100%_-_0.16em),transparent)]\">\n      <span className=\"invisible\">0</span>\n      <motion.span\n        className=\"absolute inset-x-0 top-[0.14em] flex flex-col\"\n        initial={false}\n        animate={{ y: `${-d * 10}%` }}\n        transition={reduce ? { duration: 0 } : SPRING}\n      >\n        {DIGITS.map((n) => (\n          <span key={n} className=\"block text-center\">\n            {n}\n          </span>\n        ))}\n      </motion.span>\n    </span>\n  )\n}\n\nfunction NumberRoll({ value = 1284, format, locale = \"en-US\", trend, className }: NumberRollProps) {\n  const reduce = useReducedMotion()\n  const fmt = React.useMemo(() => new Intl.NumberFormat(locale, format), [locale, JSON.stringify(format)]) // eslint-disable-line react-hooks/exhaustive-deps\n  const text = fmt.format(value)\n  const prev = React.useRef(value)\n  const [dir, setDir] = React.useState<0 | 1 | -1>(0)\n\n  React.useEffect(() => {\n    if (!trend || value === prev.current) return\n    setDir(value > prev.current ? 1 : -1)\n    prev.current = value\n    const t = setTimeout(() => setDir(0), 900)\n    return () => clearTimeout(t)\n  }, [value, trend])\n\n  // Key characters from the right so columns keep identity as length changes.\n  const chars = text.split(\"\")\n  let digitPos = 0\n  const keyed = chars\n    .map((c, i) => ({ c, i }))\n    .reverse()\n    .map(({ c, i }) => {\n      const isDigit = c >= \"0\" && c <= \"9\"\n      const key = isDigit ? `d${digitPos++}` : `s${chars.length - i}-${c}`\n      return { c, key, isDigit }\n    })\n    .reverse()\n\n  return (\n    <span\n      data-slot=\"number-roll\"\n      className={cn(\n        \"relative inline-flex items-baseline tabular-nums transition-colors duration-500 ease-hairline\", // craft-check-ignore: slow fade back after a trend flash\n        dir === 1 && \"text-success duration-[70ms]\",\n        dir === -1 && \"text-danger duration-[70ms]\",\n        className\n      )}\n    >\n      <span className=\"sr-only\">{text}</span>\n      <span aria-hidden className=\"inline-flex items-baseline\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {keyed.map(({ c, key, isDigit }) => (\n            <motion.span\n              key={key}\n              layout={reduce ? false : \"position\"}\n              initial={reduce ? false : { opacity: 0, y: \"0.35em\", filter: \"blur(3px)\" }}\n              animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, y: \"-0.35em\", filter: \"blur(3px)\" }}\n              transition={reduce ? { duration: 0 } : SPRING}\n              className=\"inline-block\"\n            >\n              {isDigit ? <Digit d={Number(c)} reduce={reduce} /> : <span className=\"inline-block whitespace-pre\">{c}</span>}\n            </motion.span>\n          ))}\n        </AnimatePresence>\n      </span>\n    </span>\n  )\n}\n\nexport { NumberRoll }\nexport type { NumberRollProps }\n"
    }
  ]
}
