import { createContext, useCallback, useEffect, useRef, useSyncExternalStore, type RefObject } from 'react' export const DrawerScrollContainerRefContext = createContext | null>(null) export const IsScrollingDetectedByRenderedHeightContext = createContext(undefined) /** * Detects whether `containerRef`'s content overflows its box (i.e. is actually * scrollable), not whether it's currently scrolled. Cheap and re-render-safe: * - reads happen inside ResizeObserver callbacks (post-layout, pre-paint — no forced reflow) * - subscribers only notified when the boolean actually flips * - no state lives in any ancestor; fully local to whichever component calls this */ export function useIsScrollable( containerRef: RefObject | null, ): boolean { const valueRef = useRef(false) const listenersRef = useRef(new Set<() => void>()) const subscribe = useCallback((onChange: () => void) => { listenersRef.current.add(onChange) return () => listenersRef.current.delete(onChange) }, []) const getSnapshot = useCallback(() => valueRef.current, []) const getServerSnapshot = useCallback(() => false, []) useEffect(() => { const container = containerRef?.current if (!container) return const check = () => { // +1 tolerance for subpixel rounding during CSS transitions const next = container.scrollHeight > container.clientHeight + 1 if (next !== valueRef.current) { valueRef.current = next listenersRef.current.forEach((cb) => cb()) } } check() const ro = new ResizeObserver(check) ro.observe(container) // content can grow/shrink without the (overflow:scroll) container's own // box changing size, so watch the content child too if (container.firstElementChild) { ro.observe(container.firstElementChild) } return () => ro.disconnect() }, [containerRef]) return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) }