# Picker

**Maturity:** experimental

A single-select control that chooses one value from a discrete set of options.

## Import

```tsx
import { Picker } from "@liquidify/react/picker"
```

## Props

- `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`; default ``false``. Uncontrolled seed for the open state of the menu popover panel. Ignored for `segmented` and `wheel` styles. Pass `defaultOpen={true}` in a Storybook story or test to pre-open the listbox panel for visual grading without a user interaction.
- `defaultValue`: `string | undefined`; default ``items[0].value``. Uncontrolled seed for the internal selection (ignored once {@link PickerProps.value} is supplied).
- `disabled`: `boolean | undefined`. When `true`, the whole control is dimmed, removed from the tab order, and all interaction is suppressed. Since the roots vary by style (some are `<div>`, some `<button>`), `data-disabled` provides the CSS hook and `aria-disabled` is set on the appropriate ARIA root.
- `items`: `readonly PickerItem[]` (required). The selectable options, rendered in order. At least one item is required.
- `labelProps`: `LabelProps | undefined`. Per-instance typography override for the trigger value and the option labels, forwarded to the internal {@link Text} (ADR-0028). Token-backed axes only; the HIG default is `variant="body"` with `color="inherit"`, so each label adopts its host's colour (accent trigger, primary/secondary option, the wheel's selected colour transition). Applies only to string/number labels — an `<Icon>` / element label passes through unwrapped.
- `onChange`: `((next: string) => void) | undefined`. Fires with the next selected item's `value` whenever the selection changes. Always called, in both controlled and uncontrolled modes.
- `onOpenChange`: `((open: boolean) => void) | undefined`. Fires with the next open state on every open/close path of the menu popover panel — trigger click, item selection, Escape, and outside pointer-down. Ignored for `segmented` and `wheel` styles.
- `open`: `boolean | undefined`. Controlled open state of the menu popover panel (ADR-0015 presentation triad, with {@link PickerProps.defaultOpen} / {@link PickerProps.onOpenChange}). Passing it switches the open state to controlled mode: the panel follows this prop and every open/close request only notifies. Ignored for `segmented` and `wheel` styles.
- `placement`: `PickerPlacement | undefined`; default ``"top-left"``. Which trigger corner the `menu` panel anchors to — the corner picks the open side (`top-*` below the trigger, `bottom-*` above) and the trigger edge the panel aligns with (`*-left` / `*-right`), so the menu grows away from that corner (see {@link PickerPlacement}). Ignored for `segmented` and `wheel` styles.
- `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`.
- `value`: `string | undefined`. Controlled selected value. Passing this switches Picker to controlled mode: the rendered selection always reflects this value and the consumer owns it via {@link PickerProps.onChange}.
- `variant`: `PickerVariant | undefined`; default ``"menu"``. The visual variant of the picker — maps to SwiftUI `pickerStyle`.

## SwiftUI mapping

- ``Picker(_ title:, selection: Binding<V>, content:)`` → ``aria-label` / `aria-labelledby` + `value?: string` + `onChange(next: string)`` (direct): `title` becomes the accessible name (via `aria-label`/`aria-labelledby`); `selection: Binding<V>` splits into `value` (controlled) + `onChange`; single-select, string-keyed (ADR-0015).
- ``@State sel = 1` seed` → ``defaultValue?: string` (default `items[0].value`)` (direct): Uncontrolled seed; mirrors Segmented exactly.
- ``Text(opts[i]).tag(i)`` → ``items: PickerItem[]` where `PickerItem = { value: string; label: ReactNode; disabled?: boolean; icon?: ReactNode }`` (direct): `.tag` → `value`; `Text`/`Label` → `label`; `Label(systemImage:)` → `icon`. A string/number `label` renders through the `Text` primitive (`variant="body"`, `color="inherit"` so each option/trigger keeps its own colour authority — accent trigger, primary/secondary option, the wheel selected-colour transition); an element label passes through unwrapped (ADR-0028).
- ``.font(_:)` / `.foregroundStyle(_:)` on labels` → ``labelProps` (`Pick<TextProps, …>`)` (direct): Per-instance typography override forwarded to the internal `Text` of the trigger value **and** every option label (token-backed axes only; ADR-0028). Spread last, so an explicit value wins. Applies only to string/number labels.
- ``.pickerStyle(.menu)` / `.wheel` / `.segmented` / `.radioGroup`` → ``variant?: PickerVariant` (`"menu" | "wheel" | "segmented" | "radioGroup"`)` (direct): The whole variant axis. Default `"menu"`. `radioGroup` is the SwiftUI macOS/visionOS `.radioGroup` style, rendered web-side with the iOS checkmark-row idiom (resolved ADR-0014/0027) — a vertical native-radio group with a trailing flat token-filled checkmark indicator.
- ``.radioGroup` selection idiom (`Picker(selection:)` single `Binding<Hashable>`)` → `reuses `value` / `defaultValue` / `onChange` (value-bearing **string** triad, NOT the boolean `checked` triad)` (direct): `radioGroup` maps its single-select `Binding` to the **same string `value` triad** as `menu`/`wheel`/`segmented` and to Segmented — never Toggle's `checked`. The resolved `value` maps to the checked radio row; a controlled `value` absent from `items` renders no checked row (no raw-key echo).
- `menu presentation state (no SwiftUI counterpart — UIKit/AppKit own the menu presentation)` → ``open?: boolean` + `defaultOpen?: boolean` (default `false`) + `onOpenChange?(open: boolean)`` (direct): The `menu` panel's presentation triad (ADR-0015): `open` switches to controlled mode, `defaultOpen` seeds the uncontrolled state, `onOpenChange` fires on every open/close path (trigger click, selection, `Escape`, outside pointer-down). Delegated to the shared `Popup` primitive. Ignored by `segmented`/`wheel`.
- ``.disabled(true)`` → ``disabled?: boolean`` (direct): Whole-control disable: `data-disabled` + `aria-disabled` + `pointer-events: none` + removed from tab order. Per-item via `PickerItem.disabled`.
- ``.controlSize(_:)`` → `— (deferred; see Sizes)` (direct): No size axis in v1.
- ``.tint(_:)`` → `— (deferred; see Out of scope)` (direct): No tint axis in v1; accent is always `--lq-color-accent`.
- ``.frame(height: 150)` (wheel)` → `n/a (component-owned geometry)` (direct): iOS drum height; resolved from `--lq-picker-wheel-h`.
- ``Picker(sources:selection:…)` multi-select` → `—` (direct): Out of scope; single-select only.
