{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-number-flow",
  "title": "Animated Number Flow",
  "description": "A number whose digits roll like an odometer to the new value, with Intl formatting for currency, percent and compact notation.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/tealmac/animated-number-flow.tsx",
      "content": "// Adapted from Ballmac UI (https://github.com/vamsiy78/ballmac-ui), MIT License, Copyright (c) 2026 Ballmac.\n\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype AnimatedNumberFlowProps = Omit<\n  React.ComponentProps<\"span\">,\n  \"children\"\n> & {\n  /** The number to show. Change it and the digits roll to the new value. */\n  value: number\n  /** BCP 47 locale for grouping and decimals. Defaults to \"en-US\" so the server and browser agree. */\n  locale?: string\n  /** Intl.NumberFormat options: currency, percent, compact, fraction digits and so on. */\n  format?: Intl.NumberFormatOptions\n  /** Text before the number, such as a unit label. */\n  prefix?: string\n  /** Text after the number. */\n  suffix?: string\n  /** Read each change aloud politely. Off by default because values that change often are noisy. */\n  announce?: boolean\n  /** Fade the top and bottom edge of each rolling digit. */\n  fade?: boolean\n}\n\nconst DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n\nfunction Digit({\n  digit,\n  reduce,\n  fade,\n}: {\n  digit: number\n  reduce: boolean | null\n  fade: boolean\n}) {\n  return (\n    <motion.span\n      layout=\"position\"\n      initial={reduce ? false : { opacity: 0, scale: 0.6 }}\n      animate={{ opacity: 1, scale: 1 }}\n      exit={reduce ? undefined : { opacity: 0, scale: 0.6 }}\n      transition={{ type: \"spring\", stiffness: 300, damping: 30 }}\n      className={cn(\n        \"relative inline-grid overflow-hidden leading-none\",\n        fade &&\n          \"[mask-image:linear-gradient(to_bottom,transparent,black_22%,black_78%,transparent)]\"\n      )}\n      style={{ height: \"1em\", lineHeight: 1 }}\n    >\n      {/* An invisible zero gives the column its width. */}\n      <span className=\"invisible col-start-1 row-start-1 leading-none\">0</span>\n      <motion.span\n        className=\"col-start-1 row-start-1 flex flex-col\"\n        style={{ height: \"10em\", lineHeight: 1 }}\n        initial={false}\n        animate={{ y: `${-digit * 10}%` }}\n        transition={\n          reduce\n            ? { duration: 0 }\n            : { type: \"spring\", stiffness: 140, damping: 20, mass: 0.9 }\n        }\n      >\n        {DIGITS.map((n) => (\n          <span\n            key={n}\n            className=\"flex items-center justify-center\"\n            style={{ height: \"1em\", lineHeight: 1 }}\n          >\n            {n}\n          </span>\n        ))}\n      </motion.span>\n    </motion.span>\n  )\n}\n\nfunction AnimatedNumberFlow({\n  value,\n  locale = \"en-US\",\n  format,\n  prefix,\n  suffix,\n  announce = false,\n  fade = true,\n  className,\n  ...props\n}: AnimatedNumberFlowProps) {\n  const reduce = useReducedMotion()\n  const formatter = React.useMemo(\n    () => new Intl.NumberFormat(locale, format),\n    [locale, format]\n  )\n  const parts = formatter.formatToParts(Number.isFinite(value) ? value : 0)\n  const text = `${prefix ?? \"\"}${formatter.format(Number.isFinite(value) ? value : 0)}${suffix ?? \"\"}`\n\n  // Digits are keyed by place value counted from the decimal point, so a ones column stays a ones column\n  // when a thousands column appears or disappears.\n  const intDigits = parts\n    .filter((p) => p.type === \"integer\")\n    .reduce((n, p) => n + p.value.length, 0)\n  let intSeen = 0\n  let fracSeen = 0\n  const nodes: React.ReactNode[] = []\n\n  parts.forEach((part, index) => {\n    if (part.type === \"integer\") {\n      for (const ch of part.value)\n        nodes.push(\n          <Digit\n            key={`i${intDigits - 1 - intSeen++}`}\n            digit={Number(ch)}\n            reduce={reduce}\n            fade={fade}\n          />\n        )\n    } else if (part.type === \"fraction\") {\n      for (const ch of part.value)\n        nodes.push(\n          <Digit\n            key={`f${fracSeen++}`}\n            digit={Number(ch)}\n            reduce={reduce}\n            fade={fade}\n          />\n        )\n    } else {\n      nodes.push(\n        <motion.span\n          key={`${part.type}${index}`}\n          layout=\"position\"\n          className=\"inline-block whitespace-pre\"\n        >\n          {part.value}\n        </motion.span>\n      )\n    }\n  })\n\n  return (\n    <span\n      data-slot=\"animated-number-flow\"\n      className={cn(\n        \"inline-flex items-center align-baseline tabular-nums\",\n        className\n      )}\n      {...props}\n    >\n      <span className=\"sr-only\" aria-live={announce ? \"polite\" : undefined}>\n        {text}\n      </span>\n      <span aria-hidden=\"true\" className=\"inline-flex items-center\">\n        {prefix && <span className=\"me-[0.25em] whitespace-pre\">{prefix}</span>}\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {nodes}\n        </AnimatePresence>\n        {suffix && <span className=\"ms-[0.25em] whitespace-pre\">{suffix}</span>}\n      </span>\n    </span>\n  )\n}\n\nexport { AnimatedNumberFlow, type AnimatedNumberFlowProps }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}