'use client'; import { useEffect, useState } from 'react'; /** * Tailwind v4 default breakpoints (rem → px at 16px base). * In v4 these are CSS variables: var(--breakpoint-sm), var(--breakpoint-md), etc. * * Usage with useMediaQuery: * useMediaQuery(`(max-width: ${BREAKPOINTS.md - 1}px)`) // < 768px * useMediaQuery(`(min-width: ${BREAKPOINTS.lg}px)`) // >= 1024px */ export const BREAKPOINTS = { sm: 640, // 40rem — phones landscape md: 768, // 48rem — tablets portrait lg: 1024, // 64rem — tablets landscape / small laptops xl: 1280, // 80rem — desktops '2xl': 1536, // 96rem } as const export type Breakpoint = keyof typeof BREAKPOINTS /** * Reactive media query hook. * * @example * const isPhone = useMediaQuery('(max-width: 639px)') * const isDark = useMediaQuery('(prefers-color-scheme: dark)') * const isLandscape = useMediaQuery('(orientation: landscape)') */ export function useMediaQuery(query: string): boolean { // Lazy initializer: read the real match on first render in the browser // so we never paint a "desktop" frame on a phone (and vice-versa) before // the first effect runs. SSR / non-browser → false. const [matches, setMatches] = useState(() => { if (typeof window === 'undefined') return false return window.matchMedia(query).matches }) useEffect(() => { const mediaQuery = window.matchMedia(query) // Re-sync once on mount in case the lazy initializer was skipped // (e.g. hydration mismatch where SSR gave false). setMatches(mediaQuery.matches) const handler = (event: MediaQueryListEvent) => setMatches(event.matches) mediaQuery.addEventListener('change', handler) return () => mediaQuery.removeEventListener('change', handler) }, [query]) return matches }