/** * useReducedMotion - 無障礙動畫偵測 Hook * ======================================== * Phase 19.A - Accessibility Motion Preference * * 遵循 WCAG 2.1 Level AA 標準: * - 偵測使用者的 prefers-reduced-motion 設定 * - 動態監聽變化 * - 提供 SSR 安全的預設值 * * 使用方式: * ```tsx * const prefersReducedMotion = useReducedMotion() * * return ( *
* ... *
* ) * ``` * * @see https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion * @see ADR-031 Omni-Terminal SSE Architecture * * @author Claude Code (首席架構師) * @version 1.0.0 * @date 2026-03-28 (台北時間) */ import { useState, useEffect } from 'react' /** Media Query 字串 */ const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)' /** * 偵測使用者是否偏好減少動畫 * * @returns {boolean} true = 使用者偏好減少動畫,應該禁用或簡化動畫 * * @example * ```tsx * function TerminalMessage({ children }) { * const prefersReducedMotion = useReducedMotion() * * return ( *
* {children} *
* ) * } * ``` */ export function useReducedMotion(): boolean { // SSR 安全預設值: false (預設有動畫) const [prefersReducedMotion, setPrefersReducedMotion] = useState(false) useEffect(() => { // 檢查瀏覽器支援 if (typeof window === 'undefined' || !window.matchMedia) { return } const mediaQuery = window.matchMedia(REDUCED_MOTION_QUERY) // 設定初始值 setPrefersReducedMotion(mediaQuery.matches) // 監聽變化 (使用者可能在系統設定中切換) const handleChange = (event: MediaQueryListEvent) => { setPrefersReducedMotion(event.matches) } // 現代瀏覽器使用 addEventListener if (mediaQuery.addEventListener) { mediaQuery.addEventListener('change', handleChange) return () => mediaQuery.removeEventListener('change', handleChange) } // 舊版瀏覽器 fallback (Safari < 14) // Note: addListener/removeListener 在新瀏覽器已棄用,但為相容 Safari < 14 保留 mediaQuery.addListener(handleChange) return () => mediaQuery.removeListener(handleChange) }, []) return prefersReducedMotion } /** * 取得動畫類名 (自動考慮 reduced motion) * * @param animationClass - 動畫類名 (e.g., 'animate-terminal-open') * @param fallbackClass - reduced motion 時的替代類名 (預設無動畫) * @returns 適當的類名 * * @example * ```tsx * function Terminal() { * const getMotionClass = useMotionClass() * * return ( *
* ... *
* ) * } * ``` */ export function useMotionClass(): ( animationClass: string, fallbackClass?: string ) => string { const prefersReducedMotion = useReducedMotion() return (animationClass: string, fallbackClass: string = '') => { return prefersReducedMotion ? fallbackClass : animationClass } } /** * 取得動畫時長 (reduced motion 時返回 0) * * @param durationMs - 正常動畫時長 (毫秒) * @returns 適當的時長 * * @example * ```tsx * function AnimatedComponent() { * const getDuration = useMotionDuration() * * useEffect(() => { * const timer = setTimeout(() => { * // 動畫完成後的操作 * }, getDuration(300)) * * return () => clearTimeout(timer) * }, [getDuration]) * } * ``` */ export function useMotionDuration(): (durationMs: number) => number { const prefersReducedMotion = useReducedMotion() return (durationMs: number) => { return prefersReducedMotion ? 0 : durationMs } } export default useReducedMotion