import { ref, onMounted, onBeforeUnmount, shallowReadonly, type Ref } from 'vue' export interface MediaQueryOptions { /** * A custom media query string. * Example: "(max-width: 640px) or (hover: none)" * If provided, takes precedence over maxWidth. */ query?: string /** * Max width in px considered true. * Ignored if `query` is provided. Default: 768. */ maxWidth?: number } /** * Reactive state of a CSS media query. */ export function useMediaQuery(options: MediaQueryOptions = {}): Readonly> { const query = options.query ?? `(max-width: ${options.maxWidth ?? 1024}px)` const matches = ref(false) let mql: MediaQueryList | null = null let onChange: ((ev: MediaQueryListEvent) => void) | null = null const setup = () => { if ('undefined' === typeof window || !window.matchMedia) { return } mql = window.matchMedia(query) matches.value = mql.matches onChange = ev => { matches.value = ev.matches } if ('addEventListener' in mql) { mql.addEventListener('change', onChange as EventListener) return } // @ts-expect-error legacy Safari mql.addListener(onChange) } const teardown = () => { if (!mql || !onChange) return if ('removeEventListener' in mql) { mql.removeEventListener('change', onChange as EventListener) } else { // @ts-expect-error legacy Safari mql.removeListener(onChange) } mql = null onChange = null } onMounted(setup) onBeforeUnmount(teardown) return shallowReadonly(matches) }