import * as React from "react" import { useSyncExternalStore } from "react" const MOBILE_BREAKPOINT = 768 /** Matches Tailwind `max-md` / desktop branch `md:` (min-width: 768px). */ const MOBILE_MQ = `(max-width: ${MOBILE_BREAKPOINT - 1}px)` type Listener = () => void let mobileListeners: Set | null = null let mobileCached = false function readIsMobile(): boolean { if (typeof window === "undefined") return false return window.matchMedia(MOBILE_MQ).matches } function ensureMobileInitialized() { if (mobileListeners) return mobileListeners = new Set() if (typeof window === "undefined") return mobileCached = readIsMobile() const notify = () => { const next = readIsMobile() if (next === mobileCached) return mobileCached = next mobileListeners?.forEach(cb => cb()) } const mql = window.matchMedia(MOBILE_MQ) mql.addEventListener("change", notify) window.addEventListener("resize", notify, { passive: true }) window.visualViewport?.addEventListener("resize", notify, { passive: true }) } function subscribeIsMobile(callback: Listener): () => void { ensureMobileInitialized() mobileListeners!.add(callback) return () => { mobileListeners!.delete(callback) } } function getIsMobileSnapshot(): boolean { ensureMobileInitialized() // Always re-read so tests that mock `matchMedia` (and live viewport // changes that race the cache) stay correct. Cache is only for notify // debounce. const next = readIsMobile() mobileCached = next return next } function getIsMobileServerSnapshot(): boolean { return false } /** * Shared mobile breakpoint (`max-width: 767px`). One window/matchMedia * subscription for the whole app (vercel-react-best-practices: * client-event-listeners). */ export function useIsMobile() { return useSyncExternalStore( subscribeIsMobile, getIsMobileSnapshot, getIsMobileServerSnapshot, ) }