export type MotionPreference = "system" | "reduce" | "no-preference"; export const MOTION_STORAGE_KEY = "mechanica:motion-preference"; export const MOTION_CHANGE_EVENT = "mechanica:motion-preference-change"; const VALID_PREFERENCES: readonly MotionPreference[] = [ "system", "reduce", "no-preference", ]; export function isMotionPreference(value: unknown): value is MotionPreference { return typeof value === "string" && VALID_PREFERENCES.includes(value as MotionPreference); } export function readMotionPreference( storageKey: string = MOTION_STORAGE_KEY, ): MotionPreference { if (typeof window === "undefined") { return "system"; } try { const stored = window.localStorage.getItem(storageKey); return isMotionPreference(stored) ? stored : "system"; } catch { return "system"; } } export function writeMotionPreference( preference: MotionPreference, storageKey: string = MOTION_STORAGE_KEY, ): void { if (typeof window === "undefined") { return; } try { if (preference === "system") { window.localStorage.removeItem(storageKey); } else { window.localStorage.setItem(storageKey, preference); } } catch { // Ignore storage failures. The DOM attribute still reflects the active choice. } applyReducedMotionToDocument(resolveReducedMotion(preference)); window.dispatchEvent( new CustomEvent(MOTION_CHANGE_EVENT, { detail: preference }), ); } export function getSystemReducedMotion(): boolean { if (typeof window === "undefined" || typeof window.matchMedia !== "function") { return false; } return window.matchMedia("(prefers-reduced-motion: reduce)").matches; } export function resolveReducedMotion(preference: MotionPreference = readMotionPreference()): boolean { if (preference === "reduce") { return true; } if (preference === "no-preference") { return false; } return getSystemReducedMotion(); } export function applyReducedMotionToDocument(reducedMotion: boolean): void { if (typeof document === "undefined") { return; } document.documentElement.dataset.reducedMotion = reducedMotion ? "true" : "false"; document.documentElement.classList.toggle("motion-reduce", reducedMotion); document.documentElement.classList.toggle("motion-safe", !reducedMotion); } export function subscribeMotionPreference(listener: () => void): () => void { if (typeof window === "undefined") { return () => undefined; } const handleChange = () => listener(); window.addEventListener(MOTION_CHANGE_EVENT, handleChange); window.addEventListener("storage", handleChange); let removeMediaListener = () => undefined; if (typeof window.matchMedia === "function") { const media = window.matchMedia("(prefers-reduced-motion: reduce)"); if (typeof media.addEventListener === "function") { media.addEventListener("change", handleChange); removeMediaListener = () => media.removeEventListener("change", handleChange); } else { const legacyMedia = media as MediaQueryList & { addListener?: (callback: (event: MediaQueryListEvent) => void) => void; removeListener?: (callback: (event: MediaQueryListEvent) => void) => void; }; if (legacyMedia.addListener && legacyMedia.removeListener) { legacyMedia.addListener(handleChange); removeMediaListener = () => legacyMedia.removeListener?.(handleChange); } } } return () => { window.removeEventListener(MOTION_CHANGE_EVENT, handleChange); window.removeEventListener("storage", handleChange); removeMediaListener(); }; }