"use client"; import { useEffect, type CSSProperties, type ReactNode } from "react"; import { AnimatePresence, motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "motion/react"; type ProgressOrbitProps = { /** 0 to 100. Leave out while the amount is unknown; the ring then spins. */ value?: number; /** Name for screen readers. */ label?: string; /** Width and height in pixels. */ size?: number; /** Ring thickness in pixels. */ stroke?: number; /** Ring colour. */ accent?: string; /** Centre content. Replaces the percent. */ children?: ReactNode; }; /* ProgressOrbit: a ring that fills on a spring with a glowing head at its tip, while a small moon circles outside until the job is done. */ export function ProgressOrbit({ value, label = "Progress", size = 120, stroke = 8, accent = "#a78bfa", children }: ProgressOrbitProps) { const reduce = useReducedMotion(); const unknown = value === undefined; const target = Math.min(100, Math.max(0, value ?? 25)); const done = target === 100; const raw = useMotionValue(target); const spring = useSpring(raw, { stiffness: 90, damping: 20 }); const shown = reduce ? raw : spring; const length = useTransform(shown, (p) => p / 100); const turn = useTransform(shown, (p) => p * 3.6); const percent = useTransform(shown, (p) => `${Math.round(p)}%`); const radius = size / 2 - stroke; const head = stroke * 1.5; useEffect(() => { raw.set(target); }, [raw, target]); return (
{!done && !reduce && ( )} {done && !reduce && ( )} {children ?? (unknown ? null : {percent})}
); }