# Popover

**Maturity:** stable

A floating glass bubble, anchored to the trigger that opened it, that presents free-form content (children) beside the anchor — the web analogue of SwiftUI .popover(isPresented:attachmentAnchor:arrowEdge:content:).

## Import

```tsx
import { Popover } from "@liquidify/react/popover"
```

## Props

- `align`: `PopoverAlign | undefined`. Cross-axis alignment of the bubble. Defaults to `"center"`.
- `aria-label`: `string | undefined`. Accessible name of the panel (default `"Popover"`).
- `aria-labelledby`: `string | undefined`. Id of the element labelling the panel.
- `children`: `ReactNode` (required). Free-form body of the bubble.
- `className`: `string | undefined`. Optional additional CSS class merged into the glass surface class. The supported per-instance customisation seam: override any `--lq-*` custom property from this class's CSS — the token cascade the engine reads (ADR-0004). No raw inline style is involved, so this stays ADR-0003 clean.
- `defaultOpen`: `boolean | undefined`. Uncontrolled seed for the open state (default `false`).
- `material`: `"frosted" | "regular" | "clear" | undefined`; default `enclosing `<Material default>`, else `"regular"``. Material of the glass surface — `frosted · regular · clear` (§06). Forwarded to the engine, which resolves the chain `prop ?? <Material default> ?? "regular"`, so a bare surface inherits the enclosing provider default.
- `onOpenChange`: `((open: boolean) => void) | undefined`. Fires with the next open state on every open/close path (Escape, outside tap, focus-loss).
- `open`: `boolean | undefined`. Controlled open state. Passing it switches Popover to controlled mode.
- `perf`: `"low" | "high" | undefined`; default `derived from `<Backdrop type>`, else `"low"``. Perf budget of the glass surface (ADR-0006/0007). Forwarded to the engine as `data-glass-perf`; `"high"` opts this surface into the edge-lensing filter. When omitted, derives from the enclosing `<Backdrop type>` (§04).
- `placement`: `LogicalSide | undefined`. Which side of the trigger the bubble sits on. Defaults to `"bottom"`.
- `ref`: `Ref<HTMLDivElement> | undefined`. Forwarded ref to the rendered element — the component's native root (or the merged child element on a component that opts into {@link SlottableProps}). Generic over `E` (charter `docs/api-conventions.md` §4 rule 1, default `HTMLElement`) so a component with a polymorphic root (Link's navigation / action mode union, Text's `as`) can instantiate the concrete element per variant instead of leaving every consumer with a widened `Ref<HTMLElement>`. A component that does not narrow simply inherits the `HTMLElement` default — source-compatible with every pre-existing non-generic `extends ControlProps`.
- `trigger`: `ReactNode` (required). The element that opens the popover — merged onto `Popup.Trigger` via `asChild`.

## SwiftUI mapping

- ``isPresented: Binding<Bool>`` → ``open` / `defaultOpen` / `onOpenChange`` (direct): presentation triad (ADR-0015); controlled iff `open !== undefined` (audit §8); forwarded to `Popup` as controlled
- `(the modified view)` → ``trigger: ReactNode`` (direct): the anchor element, merged via `Popup.Trigger asChild` (ADR-0016)
- ``content: () -> View`` → ``children: ReactNode`` (direct): free-form body rendered inside `Popup.Content` (contrast ActionSheet, which takes structured `actions[]`, not `children`); `color="inherit"` (ADR-0028)
- ``arrowEdge: Edge` (default `.top`)` → ``placement?: "top" | "bottom" | "left" | "right"`` (direct): forwarded to `Popup`. The arrow/caret nub itself is **not rendered on web** (intentional — Popup has no caret), so `arrowEdge`'s only surviving meaning is which trigger side the panel sits on, expressed via `placement`. **Inversion (load-bearing):** SwiftUI `arrowEdge` names the *bubble* edge that faces the anchor; Popup `placement` names the *trigger* side the panel sits on, so SwiftUI `arrowEdge: .top` (bubble's top edge against the anchor, i.e. bubble below it) → React `placement: "bottom"`. `placement` stays Popup-native, not a copy of SwiftUI's edge naming. Default `"bottom"` with Popup collision auto-flip (see open questions).
- ``attachmentAnchor: .point(.center)`` → ``align?: "start" | "center" | "end"` (`"center"` default)` (direct): Popup's cross-axis alignment; `.point(.center)` maps to centred alignment and `.rect(.bounds)` (SwiftUI default) ≈ Popup's default rect anchoring. A distinct point-vs-bounds `attachmentAnchor` prop is **not** introduced for v1 (open question).
- ``presentationCompactAdaptation(.popover)`` → `(no prop — always anchored on web)` (direct): v1 renders Popover as an always-anchored popover; there is no compact/regular size-class swap on the web, so the "adapts to sheet" default is dropped. A responsive `<Sheet>` swap is deferred (Out of scope; open question).
- `(presentation chrome)` → ``material?` / `perf?`` (direct): `PresentationalProps` (ADR-0023) — the bubble's glass surface (default `frosted`); the base is tint-free (no `tint` axis).
- ``label` on the modified view` → ``aria-label` / `aria-labelledby`` (direct): accessible name of the dialog panel; the demo body has no heading, so a name must be supplied (default `"Popover"`).
- `(no SwiftUI analogue)` → ``ref?: Ref<HTMLDivElement>`` (web-only): `PopoverProps extends PresentationalProps<HTMLDivElement>` — forwarded to the semantic root, the portalled `role="dialog"` glass panel; attached while mounted, `null` while closed.
