'use client'; import { useSyncExternalStore } from 'react'; /** * Viewport width below which we consider the client a phone. * * Deliberately the complement of Tailwind's `sm` breakpoint (min-width: 640px), * so `useIsMobile()` and a `sm:` class always agree about which side of the * breakpoint we are on. Keep the two in sync if the Tailwind theme changes. */ export const MOBILE_MEDIA_QUERY = '(max-width: 639px)'; function hasMatchMedia(): boolean { return typeof window !== 'undefined' && typeof window.matchMedia === 'function'; } function subscribe(onStoreChange: () => void): () => void { if (!hasMatchMedia()) return () => {}; const mediaQueryList = window.matchMedia(MOBILE_MEDIA_QUERY); mediaQueryList.addEventListener('change', onStoreChange); return () => mediaQueryList.removeEventListener('change', onStoreChange); } function getSnapshot(): boolean { if (!hasMatchMedia()) return false; return window.matchMedia(MOBILE_MEDIA_QUERY).matches; } function getServerSnapshot(): boolean { // No viewport exists while prerendering, so assume desktop. React re-reads the // real snapshot right after hydration, which cannot produce a mismatch warning. return false; } /** * Reactive "is this a phone-sized viewport?" flag. * * Use this only where a Tailwind responsive class cannot do the job — i.e. when a * breakpoint has to drive JavaScript rather than CSS. The motivating case is * Plotly, whose layout is a plain JS object that no stylesheet can reach. * For anything expressible as `class="… sm:…"`, prefer the Tailwind class: * CSS media queries need no hydration and never flash the wrong state. * * Notes: * - SSR-safe. Returns `false` during prerender and on the hydration pass, then * settles to the real value. Never touches `window` at module scope. * - Subscribes to the media query, so rotating a device or resizing a window * re-renders the consumer. * - Returns a primitive, so `useSyncExternalStore` will not loop on identity. * * @returns `true` when the viewport is narrower than Tailwind's `sm` breakpoint. */ export function useIsMobile(): boolean { return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); } export default useIsMobile;