04

Overlay lifecycle

mount → closing → unmount. The node stays mounted through its 100ms exit and unmounts on a matching animationend (with a timeout fallback). Reduced motion short-circuits the closing phase entirely.

useOverlayPhase()anim-menu-in/outanim-dialog-in/outanim-scrim-in/out

Anchored menus HINGE from their anchored edge (scaleY from the top); centered dialogs SCALE from center. Both enter on the pop spring — both leave in 100ms, flat, no spring. And the scrim always enters one tier faster than its panel.

Anchored menus — single-open bus

open one, then the other: peers close themselves. Esc / outside-click dismisses.

Centered dialog

scrim: 160ms base · panel: 380ms pop spring · exits together in 100ms

The two-phase contract

closing keeps the DOM alive for its exit

const o = useOverlayPhase();          // closed → open → closing → closed
{o.isMounted && (
  <div ref={o.panelRef}
       className={o.isClosing ? "anim-menu-out" : "anim-menu-in"} />
)}
// unmounts on animationend matching /-out$/ — 240ms timeout as a fallback;
// prefers-reduced-motion skips "closing" and unmounts immediately