import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from 'react'; export type MotionPreference = 'system' | 'reduce' | 'no-preference'; export interface MotionPreferenceContextValue { preference: MotionPreference; systemPrefersReducedMotion: boolean; reduceMotion: boolean; setPreference: (preference: MotionPreference) => void; toggleReducedMotion: () => void; } export interface ReducedMotionProviderProps { children: ReactNode; defaultPreference?: MotionPreference; storageKey?: string; persist?: boolean; } const DEFAULT_STORAGE_KEY = 'mechanica:motion-preference'; const MotionPreferenceContext = createContext(undefined); const fallbackContext: MotionPreferenceContextValue = { preference: 'system', systemPrefersReducedMotion: false, reduceMotion: false, setPreference: () => undefined, toggleReducedMotion: () => undefined, }; export function ReducedMotionProvider({ children, defaultPreference = 'system', storageKey = DEFAULT_STORAGE_KEY, persist = true, }: ReducedMotionProviderProps): JSX.Element { const [preference, setPreferenceState] = useState(() => { if (!persist) { return defaultPreference; } return readStoredPreference(storageKey) ?? defaultPreference; }); const [systemPrefersReducedMotion, setSystemPrefersReducedMotion] = useState( getSystemPrefersReducedMotion, ); useEffect(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return undefined; } const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); const handleChange = () => { setSystemPrefersReducedMotion(mediaQuery.matches); }; handleChange(); if (typeof mediaQuery.addEventListener === 'function') { mediaQuery.addEventListener('change', handleChange); return () => mediaQuery.removeEventListener('change', handleChange); } mediaQuery.addListener(handleChange); return () => mediaQuery.removeListener(handleChange); }, []); const setPreference = useCallback( (nextPreference: MotionPreference) => { setPreferenceState(nextPreference); if (!persist) { return; } writeStoredPreference(storageKey, nextPreference); }, [persist, storageKey], ); const reduceMotion = preference === 'reduce' || (preference === 'system' && systemPrefersReducedMotion); const toggleReducedMotion = useCallback(() => { setPreference(reduceMotion ? 'no-preference' : 'reduce'); }, [reduceMotion, setPreference]); const value = useMemo( () => ({ preference, systemPrefersReducedMotion, reduceMotion, setPreference, toggleReducedMotion, }), [preference, reduceMotion, setPreference, systemPrefersReducedMotion, toggleReducedMotion], ); return ( {children} ); } export function useMotionPreference(): MotionPreferenceContextValue { return useContext(MotionPreferenceContext) ?? fallbackContext; } export function useReducedMotion(): boolean { return useMotionPreference().reduceMotion; } export function useResolvedMotionPreference(): 'reduce' | 'no-preference' { return useReducedMotion() ? 'reduce' : 'no-preference'; } export function getSystemPrefersReducedMotion(): boolean { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return false; } return window.matchMedia('(prefers-reduced-motion: reduce)').matches; } function readStoredPreference(storageKey: string): MotionPreference | undefined { if (typeof window === 'undefined') { return undefined; } try { const value = window.localStorage.getItem(storageKey); return isMotionPreference(value) ? value : undefined; } catch { return undefined; } } function writeStoredPreference(storageKey: string, preference: MotionPreference): void { if (typeof window === 'undefined') { return; } try { window.localStorage.setItem(storageKey, preference); } catch { // Private-mode storage failures should never break viewer controls. } } function isMotionPreference(value: unknown): value is MotionPreference { return value === 'system' || value === 'reduce' || value === 'no-preference'; }