'use client'; import { useCallback, useState } from 'react'; export type PopoverPlacement = 'down' | 'up'; interface PopoverPlacementApi { placement: PopoverPlacement; /** Measure the trigger button and decide placement. Call this in * the button's onClick before toggling the popover open so the * placement is correct for the current scroll position. */ measure: () => void; } /** * Decide whether a popover anchored to a button should render below * the button (default) or above it. The "down" direction is the * common case; we flip to "up" when the button is near the bottom of * the viewport and the popover would otherwise extend below the * visible area and get hidden behind the BottomNav (z-50). * * Reserves ~80px of headroom for the BottomNav (64px) + safe-area * inset so the popover doesn't push right up against the chrome. * * The caller must still ensure the popover's parent stacking context * sits above the BottomNav in the page context (z >= 50). On its own * the popover can flip, but if its parent section is z-30 the popover * still paints behind the BottomNav. */ export function usePopoverPlacement( buttonRef: React.RefObject, estimatedHeight: number = 240, reservedForChrome: number = 80, ): PopoverPlacementApi { const [placement, setPlacement] = useState('down'); const measure = useCallback(() => { const btn = buttonRef.current; if (!btn || typeof window === 'undefined') return; const rect = btn.getBoundingClientRect(); const available = window.innerHeight - rect.bottom - reservedForChrome; setPlacement(available < estimatedHeight ? 'up' : 'down'); }, [buttonRef, estimatedHeight, reservedForChrome]); return { placement, measure }; }