{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-ticker",
  "title": "Number Ticker",
  "description": "Odometer-style animated counter with Intl formatting.",
  "dependencies": [
    "gsap"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/number-ticker.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ScrollTrigger } from \"gsap/ScrollTrigger\";\n\nimport { gsap, motionScale, useGSAP } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\nif (typeof window !== \"undefined\") gsap.registerPlugin(ScrollTrigger);\n\ntype NumberTickerProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  value: number;\n  /** Intl formatting, e.g. `{ style: \"currency\", currency: \"USD\" }` or `{ notation: \"compact\" }`. */\n  format?: Intl.NumberFormatOptions;\n  locale?: string;\n  /** Roll in on mount or when scrolled into view; later `value` changes always roll from the old value. */\n  trigger?: \"mount\" | \"inView\";\n  duration?: number;\n};\n\n/** Pieces of the formatted number, keyed from the right so columns persist as the value changes. */\nfunction pieces(text: string) {\n  const chars = [...text];\n  return chars.map((char, i) => ({ char, key: `${chars.length - i}`, digit: /\\d/.test(char) ? Number(char) : null }));\n}\n\n/**\n * An odometer: every digit is a column that rolls to its value, columns further right spin further,\n * like a mechanical counter (GSAP). Screen readers and crawlers get the plain formatted number.\n */\nfunction NumberTicker({ value, format, locale, trigger = \"inView\", duration = 1.2, className, ...props }: NumberTickerProps) {\n  const ref = React.useRef<HTMLSpanElement>(null);\n  const formatter = React.useMemo(() => new Intl.NumberFormat(locale, format), [locale, format]);\n  const text = formatter.format(value);\n  const parts = pieces(text);\n  const [initial] = React.useState(() => new Map(parts.map((p) => [p.key, p.digit ?? 0])));\n  const started = React.useRef(false);\n\n  const roll = React.useCallback(\n    (fromZero: boolean) => {\n      const root = ref.current;\n      if (!root) return;\n      const scale = motionScale(root);\n      const columns = Array.from(root.querySelectorAll<HTMLElement>('[data-slot=\"ticker-column\"]'));\n      columns.forEach((col, i) => {\n        const target = `${-Number(col.dataset.digit)}em`;\n        if (!scale) return gsap.set(col, { top: target });\n        if (fromZero) gsap.set(col, { top: 0 });\n        const d = duration + (columns.length - i) * 0.08;\n        gsap.to(col, { top: target, duration: d, ease: \"power3.out\", overwrite: true });\n        // A touch of motion blur while the column spins, gone by the time it lands.\n        gsap.fromTo(col, { filter: \"blur(0px)\" }, { keyframes: { filter: [\"blur(0px)\", \"blur(1.5px)\", \"blur(0px)\"] }, duration: d * 0.8, ease: \"none\" });\n      });\n    },\n    [duration],\n  );\n\n  useGSAP(\n    () => {\n      const root = ref.current;\n      if (!root) return;\n      if (!started.current) {\n        started.current = true;\n        if (!motionScale(root)) return;\n        if (trigger === \"mount\") roll(true);\n        else {\n          ref.current?.querySelectorAll('[data-slot=\"ticker-column\"]').forEach((c) => gsap.set(c, { top: 0 }));\n          ScrollTrigger.create({ trigger: root, start: \"top 90%\", once: true, onEnter: () => roll(false) });\n        }\n        return;\n      }\n      roll(false);\n    },\n    { scope: ref, dependencies: [text] },\n  );\n\n  return (\n    <span ref={ref} data-slot=\"number-ticker\" className={cn(\"inline-flex w-fit font-mono tabular-nums\", className)} {...props}>\n      <span className=\"sr-only\">{text}</span>\n      <span aria-hidden=\"true\" className=\"inline-flex leading-none\">\n        {parts.map((p) =>\n          p.digit === null ? (\n            <span key={p.key}>{p.char}</span>\n          ) : (\n            <span key={p.key} className=\"relative inline-block h-[1em] w-[1ch] overflow-hidden [mask-image:linear-gradient(transparent,black_12%,black_88%,transparent)]\">\n              <span\n                data-slot=\"ticker-column\"\n                data-digit={p.digit}\n                className=\"cue-digits absolute inset-x-0 block leading-none\"\n                style={{ top: `${-(initial.get(p.key) ?? 0)}em` }}\n              />\n            </span>\n          ),\n        )}\n      </span>\n    </span>\n  );\n}\n\nexport { NumberTicker };\nexport type { NumberTickerProps };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/number-ticker",
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}