# ContextMenu

**Maturity:** experimental

An invisible wrapper around arbitrary children (the "source").

## Import

```tsx
import { ContextMenu } from "@liquidify/react/context-menu"
```

## Props

- `aria-label`: `string | undefined`. Accessible name of the panel (`role="menu"`). Defaults to `"Context menu"`.
- `children`: `ReactNode` (required). The source (`children` role: `trigger`) — wrapped invisibly, wired with the three `useLongPress` open gestures.
- `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`).
- `items`: `readonly ContextMenuItem[]` (required). The menu's action rows; each carries a stable `id` (§16.6).
- `labelProps`: `LabelProps | undefined`. Per-instance typography override spread last into every item's label (ADR-0028), applied uniformly over all rows.
- `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.
- `open`: `boolean | undefined`. Controlled open state. Passing it switches ContextMenu 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 source the panel sits on. Forwarded to Popup. Defaults to `"bottom"`.
- `preview`: `(() => ReactNode) | undefined`. Render-prop; when supplied, replaces the lifted source while the menu is open.
- `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`.

## SwiftUI mapping

- `(the modified view)` → ``children: ReactNode`` (direct): the source, wrapped invisibly via `Popup.Trigger asChild`
- ``menuItems: () -> View` (Buttons)` → ``items: ContextMenuItem[]`` (direct): `{ id, label, icon?, role?, onPress?, disabled? }`; each row carries a required stable `id` (§16.6 — rows key on `id`, duplicate `id` is a dev error); activation runs `onPress` then closes
- ``.tag(_:)` / `id` (collection identity)` → ``id: string` (required, per item)` (direct): consumer-supplied stable identity; the roving cursor + reconciliation key on it
- ``Button`'s `Label(_:systemImage:)`` → ``icon?: ReactNode`` (direct): leading icon slot per item
- ``Button(role: .destructive)`` → ``role: "destructive"`` (direct): red label + icon (`--lq-color-action-destructive`)
- ``preview: () -> Preview`` → ``preview?: () => ReactNode`` (direct): render-prop; when present, replaces the lifted source while open
- `(long-press gesture, implicit)` → `(built in — long-press / right-click / keyboard)` (direct): no gesture prop; `useLongPress` wires all three trigger paths
- ``isPresented`-equivalent (implicit)` → ``open` / `defaultOpen` / `onOpenChange`` (direct): presentation triad (ADR-0015), forwarded to `Popup`
- `(menu anchor side)` → ``placement?: PopupPlacement`` (direct): default `"bottom"`; forwarded to `Popup` — the panel sits on that source edge
- `(presentation chrome)` → ``material?` / `perf?` / `className?`` (direct): `PresentationalProps` (ADR-0023) — the panel's glass surface (default `frosted`); the base is tint-free (P0.B6 — **no `tint` prop**; `tint=` type-rejects; items take the role palette)
- `(menu accessible name)` → ``aria-label?: string`` (direct): the panel's `role="menu"` accessible name; default `"Context menu"`, forwarded to `Popup.Content`
- `(per-instance typography)` → ``labelProps?: LabelProps`` (direct): charter §2 seam — `Pick<TextProps, "variant"|"weight"|"color"|"tint"|"tracking"|"transform"|"italic"|"lineHeight"|"wrap">` spread last into every item's `TextLabel` (ADR-0028), applied uniformly over all rows (rule 4)
- `(no SwiftUI analogue)` → ``ref?`` (web-only): forwarded to the **source host** `.lq-context-menu-source` (the invisible wrapper), typed `Ref<HTMLDivElement>`, always attached (open or closed) — charter §4 (concrete element, semantic-root policy)
