/** * useReducedMotion — subscribe to the OS-level * ``prefers-reduced-motion`` preference. * * Every animated component under ``phone/`` reads this hook so a * single user preference disables animation app-wide. The hook also * tracks runtime changes (macOS + iOS let users toggle the setting * without reloading), so an animation doesn't silently persist when * the user enables the preference mid-call. * * SSR-safe: returns ``false`` when ``window`` is undefined. Callers * don't need to null-guard the DOM. */ import { useEffect, useState } from 'react'; const QUERY = '(prefers-reduced-motion: reduce)'; export function useReducedMotion() { const [reduced, setReduced] = useState(() => { if (typeof window === 'undefined') return false; try { return window.matchMedia(QUERY).matches; } catch { return false; } }); useEffect(() => { if (typeof window === 'undefined') return; let mq; try { mq = window.matchMedia(QUERY); } catch { return; } const onChange = (e) => setReduced(e.matches); // Both APIs exist in the wild; prefer addEventListener on modern // browsers, fall back to the deprecated addListener for older ones // (Safari < 14). Same pattern at teardown. if ('addEventListener' in mq) { mq.addEventListener('change', onChange); return () => mq.removeEventListener('change', onChange); } const legacy = mq; legacy.addListener?.(onChange); return () => legacy.removeListener?.(onChange); }, []); return reduced; }