import { useCallback, useSyncExternalStore } from "react"; const BREAKPOINTS = { "2xl": 1536, "3xl": 1600, "4xl": 2000, lg: 1024, md: 768, sm: 640, xl: 1280,} as const; type Breakpoint = keyof typeof BREAKPOINTS; type BreakpointQuery = Breakpoint | `max-${Breakpoint}` | `${Breakpoint}:max-${Breakpoint}`; function resolveMin(value: Breakpoint | number): string { const px = typeof value === "number" ? value : BREAKPOINTS[value]; return `(min-width: ${px}px)`;} function resolveMax(value: Breakpoint | number): string { const px = typeof value === "number" ? value : BREAKPOINTS[value]; return `(max-width: ${px - 1}px)`;} function parseQuery(query: BreakpointQuery | MediaQueryInput | (string & {})): string { if (typeof query !== "string") { const parts: string[] = []; if (query.min != null) parts.push(resolveMin(query.min)); if (query.max != null) parts.push(resolveMax(query.max)); if (query.pointer === "coarse") parts.push("(pointer: coarse)"); if (query.pointer === "fine") parts.push("(pointer: fine)"); if (parts.length === 0) return "(min-width: 0px)"; return parts.join(" and "); } if (query.startsWith("(")) return query; const parts: string[] = []; for (const segment of query.split(":")) { if (segment.startsWith("max-")) { const bp = segment.slice(4); if (bp in BREAKPOINTS) parts.push(resolveMax(bp as Breakpoint)); } else if (segment in BREAKPOINTS) { parts.push(resolveMin(segment as Breakpoint)); } } return parts.length > 0 ? parts.join(" and ") : query;} function getServerSnapshot(): boolean { return false;} export type MediaQueryInput = { min?: Breakpoint | number; max?: Breakpoint | number; /** Touch-like input (finger). Use "fine" for mouse/trackpad. */ pointer?: "coarse" | "fine";}; export function useMediaQuery(query: BreakpointQuery | MediaQueryInput | (string & {})): boolean { const mediaQuery = parseQuery(query); const subscribe = useCallback( (callback: () => void) => { if (typeof window === "undefined") return () => {}; const mql = window.matchMedia(mediaQuery); mql.addEventListener("change", callback); return () => mql.removeEventListener("change", callback); }, [mediaQuery], ); const getSnapshot = useCallback(() => { if (typeof window === "undefined") return false; return window.matchMedia(mediaQuery).matches; }, [mediaQuery]); return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);} export function useIsMobile(): boolean { return useMediaQuery("max-md");}
import { useCallback, useSyncExternalStore } from "react";
const BREAKPOINTS = {
"2xl": 1536,
"3xl": 1600,
"4xl": 2000,
lg: 1024,
md: 768,
sm: 640,
xl: 1280,
} as const;
type Breakpoint = keyof typeof BREAKPOINTS;
type BreakpointQuery = Breakpoint | `max-${Breakpoint}` | `${Breakpoint}:max-${Breakpoint}`;
function resolveMin(value: Breakpoint | number): string {
const px = typeof value === "number" ? value : BREAKPOINTS[value];
return `(min-width: ${px}px)`;
}
function resolveMax(value: Breakpoint | number): string {
const px = typeof value === "number" ? value : BREAKPOINTS[value];
return `(max-width: ${px - 1}px)`;
}
function parseQuery(query: BreakpointQuery | MediaQueryInput | (string & {})): string {
if (typeof query !== "string") {
const parts: string[] = [];
if (query.min != null) parts.push(resolveMin(query.min));
if (query.max != null) parts.push(resolveMax(query.max));
if (query.pointer === "coarse") parts.push("(pointer: coarse)");
if (query.pointer === "fine") parts.push("(pointer: fine)");
if (parts.length === 0) return "(min-width: 0px)";
return parts.join(" and ");
}
if (query.startsWith("(")) return query;
const parts: string[] = [];
for (const segment of query.split(":")) {
if (segment.startsWith("max-")) {
const bp = segment.slice(4);
if (bp in BREAKPOINTS) parts.push(resolveMax(bp as Breakpoint));
} else if (segment in BREAKPOINTS) {
parts.push(resolveMin(segment as Breakpoint));
}
}
return parts.length > 0 ? parts.join(" and ") : query;
}
function getServerSnapshot(): boolean {
return false;
}
export type MediaQueryInput = {
min?: Breakpoint | number;
max?: Breakpoint | number;
/** Touch-like input (finger). Use "fine" for mouse/trackpad. */
pointer?: "coarse" | "fine";
};
export function useMediaQuery(query: BreakpointQuery | MediaQueryInput | (string & {})): boolean {
const mediaQuery = parseQuery(query);
const subscribe = useCallback(
(callback: () => void) => {
if (typeof window === "undefined") return () => {};
const mql = window.matchMedia(mediaQuery);
mql.addEventListener("change", callback);
return () => mql.removeEventListener("change", callback);
},
[mediaQuery],
);
const getSnapshot = useCallback(() => {
if (typeof window === "undefined") return false;
return window.matchMedia(mediaQuery).matches;
}, [mediaQuery]);
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
export function useIsMobile(): boolean {
return useMediaQuery("max-md");
}