{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tally","type":"registry:ui","title":"Tally","description":"Animates a number from one value to another when it enters the viewport.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/tally.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface TallyProps {\n  to: number;\n  from?: number;\n  /** Ms from start to finish. */\n  duration?: number;\n  decimals?: number;\n  separator?: string;\n  className?: string;\n}\n\nexport function Tally({\n  to,\n  from = 0,\n  duration = 2000,\n  decimals = 0,\n  separator = \",\",\n  className,\n}: TallyProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const format = (n: number) =>\n    n\n      .toLocaleString(\"en-US\", { minimumFractionDigits: decimals, maximumFractionDigits: decimals })\n      .replace(/,/g, separator);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    let raf = 0;\n    // Writes straight to the DOM: no React re-render per frame.\n    const start = () => {\n      const t0 = performance.now();\n      const tick = (now: number) => {\n        const p = Math.min((now - t0) / duration, 1);\n        const eased = 1 - Math.pow(1 - p, 3);\n        el.textContent = format(from + (to - from) * eased);\n        if (p < 1) raf = requestAnimationFrame(tick);\n      };\n      raf = requestAnimationFrame(tick);\n    };\n    const io = new IntersectionObserver(([entry]) => {\n      if (entry.isIntersecting) {\n        start();\n        io.disconnect();\n      }\n    });\n    io.observe(el);\n    return () => {\n      io.disconnect();\n      cancelAnimationFrame(raf);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [to, from, duration, decimals, separator]);\n\n  return (\n    <span ref={ref} className={className} style={{ fontVariantNumeric: \"tabular-nums\" }}>\n      {format(from)}\n    </span>\n  );\n}\n"}]}