import { createContext, type ReactNode, useContext, useEffect, useMemo, useState, } from 'react'; import * as AnimationTransportModule from './AnimationTransport'; import { createIdleAnimationBridgeSnapshot, createMachineAnimationPlayerAdapter, type AnimationBridgeSnapshot, type MachineAnimationPlayerAdapter, type MachineAnimationPlayerAdapterOptions, } from '../../animations/playerAdapter'; type AnyRecord = Record; export interface AnimationRuntimeControls { play: () => void; pause: () => void; resume: () => void; restart: () => void; stop: () => void; toggle: () => void; stepForward: (steps?: number) => void; stepBackward: (steps?: number) => void; seek: (normalizedTime: number) => void; setRpm: (rpm: number) => void; setTimeScale: (timeScale: number) => void; setLoop: (loop: boolean) => void; } export interface AnimationRuntimeBridgeContextValue { player: MachineAnimationPlayerAdapter | null; snapshot: AnimationBridgeSnapshot; controls: AnimationRuntimeControls; reducedMotion: boolean; } export interface AnimationRuntimeBridgeProps { machineId: string; animationModule: unknown; animationId?: string; autoPlay?: boolean; respectReducedMotion?: boolean; allowReducedMotionPlayback?: boolean; initialRpm?: number; initialTimeScale?: number; initialLoop?: boolean; stepCount?: number; showTransport?: boolean; className?: string; transportClassName?: string; onSnapshotChange?: (snapshot: AnimationBridgeSnapshot, player: MachineAnimationPlayerAdapter) => void; onPlaybackStateChange?: (snapshot: AnimationBridgeSnapshot) => void; renderTransport?: (context: AnimationRuntimeBridgeContextValue) => ReactNode; children?: ReactNode | ((context: AnimationRuntimeBridgeContextValue) => ReactNode); } export interface AnimationTransportSurfaceProps { context: AnimationRuntimeBridgeContextValue; className?: string; } const AnimationRuntimeContext = createContext(null); function resolveTransportComponent() { const moduleRecord = AnimationTransportModule as AnyRecord; const candidate = moduleRecord.AnimationTransport ?? moduleRecord.AnimationTransportPanel ?? moduleRecord.default; return typeof candidate === 'function' ? candidate : null; } function usePrefersReducedMotion(): boolean { const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return false; } return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }); useEffect(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return undefined; } const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); const handleChange = () => { setPrefersReducedMotion(mediaQuery.matches); }; handleChange(); if (typeof mediaQuery.addEventListener === 'function') { mediaQuery.addEventListener('change', handleChange); return () => { mediaQuery.removeEventListener('change', handleChange); }; } mediaQuery.addListener(handleChange); return () => { mediaQuery.removeListener(handleChange); }; }, []); return prefersReducedMotion; } function createNoopControls(player: MachineAnimationPlayerAdapter | null): AnimationRuntimeControls { return { play: () => player?.play(), pause: () => player?.pause(), resume: () => player?.resume(), restart: () => player?.restart(), stop: () => player?.stop(), toggle: () => player?.toggle(), stepForward: (steps?: number) => player?.stepForward(steps), stepBackward: (steps?: number) => player?.stepBackward(steps), seek: (normalizedTime: number) => player?.seek(normalizedTime), setRpm: (rpm: number) => player?.setRpm(rpm), setTimeScale: (timeScale: number) => player?.setTimeScale(timeScale), setLoop: (loop: boolean) => player?.setLoop(loop), }; } export function useAnimationRuntime(): AnimationRuntimeBridgeContextValue { const context = useContext(AnimationRuntimeContext); if (!context) { throw new Error('useAnimationRuntime must be used within AnimationRuntimeBridge.'); } return context; } export function AnimationTransportSurface({ context, className, }: AnimationTransportSurfaceProps) { const TransportComponent = resolveTransportComponent(); const { snapshot, controls, player, reducedMotion } = context; const transportProps: AnyRecord = { snapshot, state: snapshot, animationState: snapshot, player, rawPlayer: player?.rawPlayer, controls, reducedMotion, isPlaying: snapshot.isPlaying, isPaused: snapshot.isPaused, status: snapshot.status, rpm: snapshot.rpm, timeScale: snapshot.timeScale, loop: snapshot.loop, progress: snapshot.progress, normalizedTime: snapshot.normalizedTime, stepIndex: snapshot.stepIndex, stepCount: snapshot.stepCount, onPlay: controls.play, onPause: controls.pause, onResume: controls.resume, onRestart: controls.restart, onStop: controls.stop, onToggle: controls.toggle, onStep: controls.stepForward, onStepForward: controls.stepForward, onStepBackward: controls.stepBackward, onSeek: controls.seek, onProgressChange: controls.seek, onRpmChange: controls.setRpm, onRPMChange: controls.setRpm, onTimeScaleChange: controls.setTimeScale, onPlaybackRateChange: controls.setTimeScale, onLoopChange: controls.setLoop, className, }; if (TransportComponent) { const Component = TransportComponent as React.ComponentType; return ; } return ; } function DefaultAnimationTransportSurface({ context, className = '', }: AnimationTransportSurfaceProps) { const { snapshot, controls, reducedMotion } = context; return (

Animation

{snapshot.status === 'playing' ? 'Running' : snapshot.status}

{Math.round(snapshot.progress * 100)}%
{reducedMotion ? (

Reduced motion is enabled. Use step controls for a lower-motion walkthrough.

) : null}
); } export function AnimationRuntimeBridge({ machineId, animationModule, animationId, autoPlay = false, respectReducedMotion = true, allowReducedMotionPlayback = false, initialRpm, initialTimeScale, initialLoop = true, stepCount, showTransport = true, className = '', transportClassName = '', onSnapshotChange, onPlaybackStateChange, renderTransport, children, }: AnimationRuntimeBridgeProps) { const reducedMotion = usePrefersReducedMotion(); const [player, setPlayer] = useState(null); const [snapshot, setSnapshot] = useState(() => createIdleAnimationBridgeSnapshot(machineId, animationId ?? 'machine-animation'), ); useEffect(() => { const options: MachineAnimationPlayerAdapterOptions = { machineId, animationId, initialRpm, initialTimeScale, initialLoop, stepCount, reducedMotion, respectReducedMotion, allowReducedMotionPlayback, }; const nextPlayer = createMachineAnimationPlayerAdapter(animationModule, options); setPlayer(nextPlayer); setSnapshot(nextPlayer.getSnapshot()); const unsubscribe = nextPlayer.subscribe((nextSnapshot) => { setSnapshot(nextSnapshot); }); if (autoPlay && (!reducedMotion || allowReducedMotionPlayback || !respectReducedMotion)) { nextPlayer.play(); } return () => { unsubscribe(); nextPlayer.dispose(); }; }, [ allowReducedMotionPlayback, animationId, animationModule, autoPlay, initialLoop, initialRpm, initialTimeScale, machineId, reducedMotion, respectReducedMotion, stepCount, ]); useEffect(() => { player?.setReducedMotion(reducedMotion); }, [player, reducedMotion]); useEffect(() => { if (!player) { return; } onSnapshotChange?.(snapshot, player); onPlaybackStateChange?.(snapshot); }, [onPlaybackStateChange, onSnapshotChange, player, snapshot]); const controls = useMemo(() => createNoopControls(player), [player]); const context = useMemo( () => ({ player, snapshot, controls, reducedMotion, }), [controls, player, reducedMotion, snapshot], ); const renderedChildren = typeof children === 'function' ? (children as (context: AnimationRuntimeBridgeContextValue) => ReactNode)(context) : children; return (
{renderedChildren} {showTransport ? renderTransport ? renderTransport(context) : ( ) : null}
); }