{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-ticker",
  "title": "Number Ticker",
  "description": "Counts to a number rather than replacing it, in any currency or format.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/number-ticker/number-ticker.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type NumberTickerProps = Omit<\n  React.HTMLAttributes<HTMLSpanElement>,\n  \"children\"\n> & {\n  value: number\n  /** Where it counts from the first time. Defaults to zero. */\n  from?: number\n  duration?: number\n  /** Passed straight to Intl.NumberFormat, so currency and percent work. */\n  format?: Intl.NumberFormatOptions\n  locale?: string\n  /** Waits until it is on screen before counting. */\n  startOnView?: boolean\n}\n\nconst ease = (t: number) => 1 - Math.pow(1 - t, 3)\n\n/**\n * Counts to a number rather than replacing it. The final value is what the\n * element is labelled with throughout, so nothing reads out a blur of\n * intermediate numbers.\n */\nexport function NumberTicker({\n  value,\n  from = 0,\n  duration = 1400,\n  format,\n  locale,\n  startOnView = true,\n  className,\n  ...rootProps\n}: NumberTickerProps) {\n  const ref = React.useRef<HTMLSpanElement>(null)\n  const previous = React.useRef(from)\n  const [display, setDisplay] = React.useState(from)\n  const [armed, setArmed] = React.useState(!startOnView)\n\n  const formatter = React.useMemo(\n    () => new Intl.NumberFormat(locale, format),\n    [format, locale]\n  )\n\n  React.useEffect(() => {\n    if (armed) return\n    const element = ref.current\n    if (!element) return\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry?.isIntersecting) {\n          setArmed(true)\n          observer.disconnect()\n        }\n      },\n      { threshold: 0.4 }\n    )\n\n    observer.observe(element)\n    return () => observer.disconnect()\n  }, [armed])\n\n  React.useEffect(() => {\n    if (!armed) return\n\n    const start = previous.current\n    previous.current = value\n\n    if (\n      start === value ||\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    ) {\n      setDisplay(value)\n      return\n    }\n\n    let raf = 0\n    let began = 0\n\n    const step = (timestamp: number) => {\n      if (began === 0) began = timestamp\n      const t = Math.min(1, (timestamp - began) / Math.max(duration, 1))\n\n      setDisplay(start + (value - start) * ease(t))\n      if (t < 1) raf = window.requestAnimationFrame(step)\n    }\n\n    raf = window.requestAnimationFrame(step)\n    return () => window.cancelAnimationFrame(raf)\n  }, [armed, duration, value])\n\n  return (\n    <span\n      ref={ref}\n      data-slot=\"number-ticker\"\n      aria-label={formatter.format(value)}\n      className={cn(\"tabular-nums\", className)}\n      {...rootProps}\n    >\n      <span aria-hidden=\"true\">{formatter.format(display)}</span>\n    </span>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "motion",
    "number",
    "statistic"
  ],
  "type": "registry:ui"
}