"use client"; import { animate, motion, useInView, useMotionTemplate, useMotionValue, useReducedMotion, useTransform, } from "motion/react"; import { useEffect, useRef, type ReactNode } from "react"; import { cn } from "@/lib/utils"; type DonutChartProps = { /** Primary share, 0–100 (the ink wedge). */ value: number; /** Big serif figure in the centre (e.g. a ). */ centerValue?: ReactNode; /** Caption under the figure. */ centerCaption?: string; /** Diameter in px. Default 320. */ size?: number; className?: string; }; /** * DonutChart — the event-deck conic donut (part / whole split). * * Two-phase build (matches the deck): the ink wedge grows 0 → value (950 ms), * then the accent fills value → 100 (750 ms), over a punched-out paper hole. * A `` in `centerValue` should start on phase one (delay ~0.35 s). * See sd EVENT-DECK.md → donut. */ export function DonutChart({ value, centerValue, centerCaption, size = 320, className, }: DonutChartProps) { const ref = useRef(null); const inView = useInView(ref, { once: true, amount: 0.4 }); const reduce = useReducedMotion(); const pb = useMotionValue(reduce ? value : 0); // ink stop % const py = useMotionValue(reduce ? 100 : 0); // accent stop % const pbPct = useTransform(pb, (v) => `${v}%`); const pyPct = useTransform(py, (v) => `${v}%`); const background = useMotionTemplate`conic-gradient(var(--ink-900) 0 ${pbPct}, var(--accent-500) ${pbPct} ${pyPct}, var(--ink-100) ${pyPct} 100%)`; useEffect(() => { if (!inView || reduce) return; const ease = [0.22, 1, 0.36, 1] as const; const a = animate(pb, value, { duration: 0.95, delay: 0.35, ease }); const b = animate(py, value, { duration: 0.95, delay: 0.35, ease }); const c = animate(py, 100, { duration: 0.75, delay: 1.3, ease }); return () => { a.stop(); b.stop(); c.stop(); }; }, [inView, reduce, value, pb, py]); return (
{centerValue != null && (
{centerValue}
)} {centerCaption && (
{centerCaption}
)}
); }