"use client"; import { motion, useInView, useReducedMotion } from "motion/react"; import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; type IndexRollProps = { /** Target value. The roll counts up 00 → value, one step at a time. */ value: number; /** Zero-pad to this many digits. Default: digits in `value`, min 2. */ pad?: number; /** ms between each increment. Default 330 (matches the deck). */ step?: number; /** ms before the roll starts once in view. Default 420. */ startDelay?: number; className?: string; }; /** * IndexRoll — the giant section index of the event-deck level-divider. * * Counts 00 → value one unit at a time; only the digit that changes "rides up" * (translateY 0.85em → 0, opacity 0 → 1, 280 ms ease-out-expo), like an elevator. * Distinct from Counter (value tick) and LetterCounter (letter spin). * Pair with `font-display` at a large size. See sd EVENT-DECK.md → level divider. */ export function IndexRoll({ value, pad, step = 330, startDelay = 420, className }: IndexRollProps) { const ref = useRef(null); const inView = useInView(ref, { once: true, amount: 0.5 }); const reduce = useReducedMotion(); const len = Math.max(pad ?? String(Math.abs(Math.trunc(value))).length, 2); const [cur, setCur] = useState(0); useEffect(() => { if (!inView) return; if (reduce) { setCur(value); return; } setCur(0); let n = 0; let timer: ReturnType; const start = setTimeout(function tick() { n += 1; setCur(n); if (n < value) timer = setTimeout(tick, step); }, startDelay); return () => { clearTimeout(start); clearTimeout(timer); }; }, [inView, reduce, value, step, startDelay]); const chars = String(cur).padStart(len, "0").split(""); return ( {chars.map((c, i) => ( // Stable key per position; the inner span re-mounts (and re-animates) only // when its own digit changes — so untouched digits stay put. {c} ))} ); }