"use client"; import { useEffect, useId, useState } from "react"; import { useReducedMotion } from "motion/react"; type NewtonCradleProps = { /** Number of balls. */ balls?: number; /** How many balls swing at each end. */ lifted?: number; /** Colour of the wooden base. */ wood?: string; /** 1 is the default pace; 2 is twice as fast. */ speed?: number; /** Engraved on the base plate and read out by screen readers. */ label?: string; className?: string; }; const CX = 160; const TOP = 30; // Height the strings hang from. const LENGTH = 124; const R = 18; const REACH = 0.38; // Swing in radians. const REST = 0.09; /* NewtonCradle: chrome balls on V strings under a two-rail frame on a walnut base. The end ball swings in, the knock runs through the still balls, the far ball swings out. */ export function NewtonCradle({ balls = 5, lifted = 1, wood = "#6b3f22", speed = 1, label = "Loading", className = "" }: NewtonCradleProps) { const reduce = useReducedMotion(); const id = useId().replace(/:/g, ""); const [clock, setClock] = useState(REST); useEffect(() => { if (reduce) return; const seconds = 1.9 / speed; const start = performance.now() - REST * seconds * 1000; let raf = 0; const tick = (now: number) => { setClock(((now - start) / (seconds * 1000)) % 1); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [reduce, speed]); const p = reduce ? REST : clock; const swing = REACH * Math.cos(p * Math.PI * 2); const since = (p % 0.5) - 0.25; // Time after the last knock, which comes at a quarter and three quarters. const shake = since >= 0 ? 0.9 * Math.exp(-since * 26) * Math.sin(since * 120) : 0; const lift = Math.min(lifted, Math.floor(balls / 2)); const spots = Array.from({ length: balls }, (_, i) => { const home = CX + (i - (balls - 1) / 2) * R * 2; let a = 0; if (i >= balls - lift) a = Math.max(swing, 0); if (i < lift) a = Math.min(swing, 0); const still = i >= lift && i < balls - lift; return { home, x: home + LENGTH * Math.sin(a) + (still ? shake : 0), y: TOP + LENGTH * Math.cos(a), }; }); return (