{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "count-up-number",
  "title": "Count Up Number",
  "author": "henriquegpb <https://github.com/henriquegpb>",
  "description": "A stat that counts up from zero the first time it scrolls into view, easing out so the last digits settle slowly; it is sized to the finished number, so nothing around it shifts while it climbs.",
  "files": [
    {
      "path": "registry/microkit/count-up-number.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nexport function CountUpNumber({\n  value = 48290,\n  duration = 2000,\n  decimals = 0,\n  prefix = \"\",\n  suffix = \"\",\n  locale = \"en-US\",\n  className = \"\",\n}) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const shown = useRef(0);\n  const [display, setDisplay] = useState(0);\n  const [inView, setInView] = useState(false);\n  const formatter = new Intl.NumberFormat(locale, {\n    minimumFractionDigits: decimals,\n    maximumFractionDigits: decimals,\n  });\n\n  useEffect(() => {\n    const node = ref.current;\n    if (!node) return;\n    // The first report arrives on observe and says \"intersecting\" for a sliver,\n    // so the ratio is what decides the number has actually been seen.\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.intersectionRatio < 0.5) return;\n        observer.disconnect();\n        setInView(true);\n      },\n      { threshold: 0.5 },\n    );\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  useEffect(() => {\n    if (!inView) return;\n    // Counting from what is on screen lets a new value pick up mid-count instead of restarting at zero.\n    const from = shown.current;\n    const length = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches ? 0 : duration;\n    const start = performance.now();\n    let frame = 0;\n\n    const step = () => {\n      const progress = length > 0 ? Math.min(1, (performance.now() - start) / length) : 1;\n      // Expo-out covers most of the distance at once and spends the rest settling the last digits.\n      const eased = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress);\n      shown.current = from + (value - from) * eased;\n      setDisplay(shown.current);\n      if (progress < 1) frame = requestAnimationFrame(step);\n    };\n\n    frame = requestAnimationFrame(step);\n    return () => cancelAnimationFrame(frame);\n  }, [inView, value, duration]);\n\n  return (\n    <span ref={ref} className={`inline-grid whitespace-nowrap tabular-nums ${className}`}>\n      {/* The settled value sizes the box and is what screen readers and copy-paste get, but it is never painted. */}\n      <span className=\"text-transparent [grid-area:1/1] motion-reduce:text-inherit\">\n        {`${prefix}${formatter.format(value)}${suffix}`}\n      </span>\n      {/* Right-aligned over the settled value, each digit climbs in the column it will finish in. */}\n      <span\n        className=\"pointer-events-none select-none justify-self-end [grid-area:1/1] motion-reduce:invisible\"\n        aria-hidden=\"true\"\n      >\n        {`${prefix}${formatter.format(display)}${suffix}`}\n      </span>\n    </span>\n  );\n}\n\nexport function CountUpStat() {\n  return (\n    <figure className=\"m-0 grid justify-items-center gap-[14px] text-center text-[light-dark(#14181e,#f2f4f6)]\">\n      <CountUpNumber value={48290} className=\"text-[64px] font-semibold leading-none tracking-[-.045em]\" />\n      <figcaption className=\"inline-flex items-center gap-[10px] text-[14px] font-medium leading-none text-[light-dark(#5d6673,#8d939d)]\">\n        Monthly active users\n        <CountUpNumber\n          value={12.4}\n          decimals={1}\n          prefix=\"+\"\n          suffix=\"%\"\n          className=\"rounded-full bg-[light-dark(#ffedd5,rgba(249,115,22,.14))] px-[8px] py-[5px] text-[12px] font-semibold text-[light-dark(#c2410c,#fb923c)]\"\n        />\n      </figcaption>\n    </figure>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/microkit/count-up-number.tsx"
    }
  ],
  "docs": "Live preview and the CSS, JavaScript and Tailwind variants: https://microkit.co/components/count-up-number",
  "categories": [
    "data-display",
    "scroll"
  ],
  "type": "registry:component"
}