{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-number-flow",
  "title": "Animated Number Flow",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A number whose digits roll like an odometer to the new value, with locale and Intl formatting for currency, percent and compact notation, and widths that animate as digits come and go.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/animated-number-flow.tsx",
      "content": "// Ballmac UI: Animated Number Flow. https://ui.ballmac.com/components/animated-number-flow\n\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useLocale } from \"@/lib/ballmac/i18n\"\n\ntype AnimatedNumberFlowProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\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. Fixed by default 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({ digit, reduce, fade }: { digit: number; reduce: boolean | null; fade: boolean }) {\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(\"relative inline-grid overflow-hidden leading-none\", fade && \"[mask-image:linear-gradient(to_bottom,transparent,black_22%,black_78%,transparent)]\")}\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={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 140, damping: 20, mass: 0.9 }}\n      >\n        {DIGITS.map((n) => (\n          <span key={n} className=\"flex items-center justify-center\" style={{ height: \"1em\", lineHeight: 1 }}>\n            {n}\n          </span>\n        ))}\n      </motion.span>\n    </motion.span>\n  )\n}\n\nfunction AnimatedNumberFlow({\n  value,\n  locale,\n  format,\n  prefix,\n  suffix,\n  announce = false,\n  fade = true,\n  className,\n  ...props\n}: AnimatedNumberFlowProps) {\n  const defaultLocale = useLocale()\n  locale ??= defaultLocale\n  const reduce = useReducedMotion()\n  const formatter = React.useMemo(() => new Intl.NumberFormat(locale, format), [locale, format])\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.filter((p) => p.type === \"integer\").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) nodes.push(<Digit key={`i${intDigits - 1 - intSeen++}`} digit={Number(ch)} reduce={reduce} fade={fade} />)\n    } else if (part.type === \"fraction\") {\n      for (const ch of part.value) nodes.push(<Digit key={`f${fracSeen++}`} digit={Number(ch)} reduce={reduce} fade={fade} />)\n    } else {\n      nodes.push(\n        <motion.span key={`${part.type}${index}`} layout=\"position\" className=\"inline-block whitespace-pre\">\n          {part.value}\n        </motion.span>\n      )\n    }\n  })\n\n  return (\n    <span\n      data-slot=\"animated-number-flow\"\n      className={cn(\"inline-flex items-center align-baseline tabular-nums\", className)}\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",
      "target": "@components/ballmac/animated-number-flow.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "number",
      "odometer",
      "counter",
      "currency",
      "format"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "<AnimatedNumberFlow value format locale prefix suffix announce fade />. Change value and each digit rolls to its new place. Uses Intl.NumberFormat with a fixed default locale so server and browser agree.",
      "whenToUse": [
        "Prices, balances and stats that change on interaction",
        "Live metrics"
      ],
      "whenNotToUse": [
        "A one-time count-up on scroll (number-ticker)"
      ],
      "composesWith": [
        "number-ticker",
        "stat-card",
        "kpi-row"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "The formatted number is read as text; announce adds a polite live region"
        },
        {
          "keys": "Reduced motion",
          "action": "The value changes instantly"
        }
      ],
      "customization": [
        "format (Intl options)",
        "locale",
        "prefix and suffix",
        "fade",
        "announce"
      ]
    },
    "examples": [
      "animated-number-flow-demo",
      "animated-number-flow-formats"
    ],
    "url": "https://ui.ballmac.com/components/animated-number-flow"
  },
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}