"use client"; import { useReducedMotion } from "motion/react"; import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; const CELLS = 9; const MIN_PULSE_MS = 500; const MAX_PULSE_MS = 1100; function useElapsed(active: boolean) { const [ms, setMs] = useState(0); useEffect(() => { if (!active) return; const startedAt = Date.now(); const timer = setInterval(() => setMs(Date.now() - startedAt), 100); return () => clearInterval(timer); }, [active]); const seconds = ms / 1000; return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${(seconds % 60).toFixed(1)}s`; } export interface PixelLoaderProps { /** Status label shown beside the grid. Default "Thinking". */ label?: string; /** Shows a live elapsed-time counter after the label. Default false. */ showElapsed?: boolean; className?: string; } /** * A 3x3 grid of cells that twinkle on their own independent, randomized * cycles, for work that runs long enough to want a sense of progress. No * two cells share a rhythm, so nothing about it reads as a sweep or a * pattern. Pairs a shimmering label with an optional live timer. Reduced * motion holds the grid dim; the timer still ticks. */ export function PixelLoader({ label = "Thinking", showElapsed = false, className }: PixelLoaderProps) { const cellRefs = useRef<(HTMLSpanElement | null)[]>([]); const elapsed = useElapsed(showElapsed); const reduce = useReducedMotion(); useEffect(() => { if (reduce) return; const animations = cellRefs.current.map((cell) => cell?.animate([{ opacity: 0.15 }, { opacity: 1 }, { opacity: 0.15 }], { duration: MIN_PULSE_MS + Math.random() * (MAX_PULSE_MS - MIN_PULSE_MS), delay: Math.random() * MAX_PULSE_MS, iterations: Number.POSITIVE_INFINITY, easing: "ease-in-out", }), ); return () => { for (const animation of animations) animation?.cancel(); }; }, [reduce]); return (