"use client"; import { IconBrandGithub } from "@tabler/icons-react"; import { motion, useMotionValue, useSpring } from "motion/react"; import * as React from "react"; import { cn } from "@/lib/utils"; // --------------------------------------------------------------------------- // Per-digit scrolling wheel // --------------------------------------------------------------------------- const ROLLING_ITEM_COUNT = 200; function DigitWheel({ digit, itemSize = 22, delay = 0, cycles = 5, isRolling = false, reverse = false, className, onSettled, }: { digit: number; itemSize?: number; delay?: number; cycles?: number; isRolling?: boolean; reverse?: boolean; className?: string; onSettled?: () => void; }) { const sequence = React.useMemo(() => { if (isRolling) { return Array.from({ length: ROLLING_ITEM_COUNT }, (_, i) => ({ id: `r${i}`, value: i % 10, })); } const seq = Array.from({ length: cycles * 10 }, (_, i) => ({ id: `s${i}`, value: (i * 7 + 3) % 10, })); const target = { id: "target", value: digit }; if (reverse) { seq.unshift(target); } else { seq.push(target); } return seq; }, [digit, cycles, isRolling, reverse]); const maxOffset = (sequence.length - 1) * itemSize; const endY = reverse ? 0 : -maxOffset; const rollingStartItem = React.useRef(0); const startOffset = rollingStartItem.current * itemSize; const y = useMotionValue( isRolling ? (reverse ? -(maxOffset - startOffset) : -startOffset) : reverse ? -maxOffset : 0 ); const ySpring = useSpring( y, isRolling ? { stiffness: 10000, damping: 500 } : { stiffness: 70, damping: 20 } ); const settledRef = React.useRef(false); const wasRollingRef = React.useRef(isRolling); // Jump y to settling start position when transitioning from rolling → settled React.useLayoutEffect(() => { if (wasRollingRef.current && !isRolling) { y.jump(reverse ? -maxOffset : 0); } wasRollingRef.current = isRolling; }, [isRolling, reverse, maxOffset, y]); // Rolling: drive y continuously via RAF (stiff spring tracks it transparently) React.useEffect(() => { if (!isRolling) return; const cycleHeight = 10 * itemSize; const msPerCycle = 1000; let startTime: number | null = null; let rafId: number; const tick = (time: number) => { if (startTime === null) startTime = time; const elapsed = time - startTime; const speed = cycleHeight / msPerCycle; const travel = elapsed * speed + startOffset; if (reverse) { y.set(Math.min(-maxOffset + travel, 0)); } else { y.set(Math.max(-travel, -maxOffset)); } rafId = requestAnimationFrame(tick); }; rafId = requestAnimationFrame(tick); return () => cancelAnimationFrame(rafId); }, [isRolling, itemSize, reverse, y, maxOffset, startOffset]); // Settling: spring to endY after delay React.useEffect(() => { if (isRolling) return; settledRef.current = false; const timer = setTimeout(() => y.set(endY), delay); return () => clearTimeout(timer); }, [endY, y, delay, isRolling]); // Detect settled React.useEffect(() => { if (isRolling) return; const unsub = ySpring.on("change", (latest) => { if (!settledRef.current && Math.abs(latest - endY) < 0.5) { settledRef.current = true; onSettled?.(); } }); return unsub; }, [ySpring, endY, onSettled, isRolling]); return (