{"slug":"number-ticker","name":"Number Ticker","description":"Slot-machine rolling digits with staggered entry.","category":"motion","source_url":"https://beui.saura3h.xyz/r/number-ticker/raw","detail_url":"https://beui.saura3h.xyz/r/number-ticker","raw_url":"https://beui.saura3h.xyz/r/number-ticker/raw","page_url":"https://beui.saura3h.xyz/components/motion/number-ticker","dependencies":["motion","react"],"internal":["@/components/motion/number-ticker","@/lib/utils"],"files":[{"path":"components/motion/number-ticker.tsx","type":"component","content":"\"use client\";\n\nimport { motion, useInView } from \"motion/react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface NumberTickerProps {\n  value: number;\n  /** Digits to pad to (left). */\n  pad?: number;\n  /** Per-digit roll duration in seconds. */\n  duration?: number;\n  /** Stagger between digits. */\n  stagger?: number;\n  /** Render only after the element enters the viewport. */\n  startOnView?: boolean;\n  prefix?: string;\n  suffix?: string;\n  className?: string;\n  digitClassName?: string;\n  /** Insert locale group separators (commas). Server-component safe. */\n  locale?: boolean;\n  /** Custom formatter. Client-only — server components must use `locale` instead. */\n  format?: (value: number) => string;\n}\n\nconst DIGIT_HEIGHT_EM = 1.1;\n\nexport function NumberTicker({\n  value,\n  pad,\n  duration = 0.9,\n  stagger = 0.04,\n  startOnView = true,\n  prefix,\n  suffix,\n  className,\n  digitClassName,\n  locale,\n  format,\n}: NumberTickerProps) {\n  const containerRef = useRef<HTMLSpanElement>(null);\n  const inView = useInView(containerRef, { once: true, amount: 0.6 });\n  const [armed, setArmed] = useState(!startOnView);\n\n  useEffect(() => {\n    if (startOnView && inView) setArmed(true);\n  }, [startOnView, inView]);\n\n  const text = useMemo(() => {\n    const rounded = Math.round(value);\n    const formatted = format\n      ? format(rounded)\n      : locale\n        ? rounded.toLocaleString()\n        : rounded.toString();\n    return pad ? formatted.padStart(pad, \"0\") : formatted;\n  }, [value, pad, format, locale]);\n\n  return (\n    <span\n      ref={containerRef}\n      className={cn(\"inline-flex items-center tabular-nums\", className)}\n      aria-label={`${prefix ?? \"\"}${text}${suffix ?? \"\"}`}\n    >\n      {prefix ? <span>{prefix}</span> : null}\n      {text.split(\"\").map((char, i) => {\n        const isDigit = /\\d/.test(char);\n        if (!isDigit) {\n          return (\n            <span key={`s-${i}`} className=\"inline-block\">\n              {char}\n            </span>\n          );\n        }\n        const digit = Number(char);\n        return (\n          <Digit\n            key={`d-${i}`}\n            digit={armed ? digit : 0}\n            delay={i * stagger}\n            duration={duration}\n            className={digitClassName}\n          />\n        );\n      })}\n      {suffix ? <span>{suffix}</span> : null}\n    </span>\n  );\n}\n\nfunction Digit({\n  digit,\n  delay,\n  duration,\n  className,\n}: {\n  digit: number;\n  delay: number;\n  duration: number;\n  className?: string;\n}) {\n  return (\n    <span\n      className={cn(\"relative inline-block overflow-hidden\", className)}\n      style={{ height: `${DIGIT_HEIGHT_EM}em`, width: \"1ch\" }}\n    >\n      <motion.span\n        initial={{ y: 0 }}\n        animate={{ y: `-${digit * DIGIT_HEIGHT_EM}em` }}\n        transition={{ duration, delay, ease: [0.16, 1, 0.3, 1] }}\n        className=\"absolute inset-x-0 top-0 flex flex-col items-center\"\n      >\n        {Array.from({ length: 10 }, (_, n) => (\n          <span key={n} className=\"flex h-[1.1em] items-center justify-center leading-none\">\n            {n}\n          </span>\n        ))}\n      </motion.span>\n    </span>\n  );\n}\n"},{"path":"components/previews/motion/number-ticker.preview.tsx","type":"preview","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { NumberTicker } from \"@/components/motion/number-ticker\";\n\nexport function NumberTickerPreview() {\n  const [value, setValue] = useState(48273);\n  useEffect(() => {\n    const id = setInterval(() => setValue((v) => v + Math.floor(Math.random() * 50)), 2500);\n    return () => clearInterval(id);\n  }, []);\n  return (\n    <div className=\"flex flex-col items-center gap-3\">\n      <p className=\"text-xs text-(--color-fg-muted)\">Active users</p>\n      <NumberTicker\n        value={value}\n        prefix=\"\"\n        className=\"text-4xl font-semibold tracking-tight text-(--color-fg) tabular-nums\"\n        format={(n) => n.toLocaleString()}\n      />\n      <p className=\"text-xs text-(--color-fg-muted)\">live · updates every 2.5s</p>\n    </div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"util","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}