{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-ticker",
  "title": "Number Ticker",
  "description": "Counts up or down to a target number when it scrolls into view.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/tealmac/number-ticker.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, type ComponentPropsWithoutRef } from \"react\"\nimport { useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface NumberTickerProps extends ComponentPropsWithoutRef<\"span\"> {\n  value: number\n  startValue?: number\n  direction?: \"up\" | \"down\"\n  delay?: number\n  decimalPlaces?: number\n}\n\n/**\n * NumberTicker counts from a start value to a target the first time it enters\n * the viewport. Implementation is a requestAnimationFrame loop with a cubic\n * ease-out curve; visibility is detected with IntersectionObserver.\n */\nexport function NumberTicker({\n  value,\n  startValue = 0,\n  direction = \"up\",\n  delay = 0,\n  className,\n  decimalPlaces = 0,\n  ...props\n}: NumberTickerProps) {\n  const ref = useRef<HTMLSpanElement>(null)\n  const reducedMotion = useReducedMotion()\n  const from = direction === \"down\" ? value : startValue\n  const to = direction === \"down\" ? startValue : value\n\n  useEffect(() => {\n    const node = ref.current\n    if (!node) return\n\n    const format = (n: number) =>\n      Intl.NumberFormat(\"en-US\", {\n        minimumFractionDigits: decimalPlaces,\n        maximumFractionDigits: decimalPlaces,\n      }).format(Number(n.toFixed(decimalPlaces)))\n\n    node.textContent = format(from)\n\n    let raf = 0\n    let timer: ReturnType<typeof setTimeout> | null = null\n    let started = false\n\n    const run = () => {\n      const duration = 1500\n      let origin: number | null = null\n      const tick = (now: number) => {\n        if (origin === null) origin = now\n        const t = Math.min((now - origin) / duration, 1)\n        const eased = 1 - Math.pow(1 - t, 3)\n        node.textContent = format(from + (to - from) * eased)\n        if (t < 1) raf = requestAnimationFrame(tick)\n      }\n      raf = requestAnimationFrame(tick)\n    }\n\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting && !started) {\n          started = true\n          if (reducedMotion) {\n            node.textContent = format(to)\n            return\n          }\n          timer = setTimeout(run, delay * 1000)\n        }\n      },\n      { threshold: 0 }\n    )\n    io.observe(node)\n\n    return () => {\n      io.disconnect()\n      if (timer !== null) clearTimeout(timer)\n      cancelAnimationFrame(raf)\n    }\n  }, [from, to, delay, decimalPlaces, reducedMotion])\n\n  return (\n    <span\n      ref={ref}\n      className={cn(\"inline-block tabular-nums\", className)}\n      {...props}\n    >\n      {startValue}\n    </span>\n  )\n}\n\nexport default NumberTicker\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}