# SegmentedControl

**Maturity:** experimental

A horizontal track of 2–N mutually-exclusive segments where exactly one is selected — the canonical glass segmented picker.

## Import

```tsx
import { SegmentedControl } from "@liquidify/react/segmented-control"
```

## Props

- `children`: `ReactNode` (required). The segments — 2..N {@link SegmentedControl.Item} children, rendered equal-width (children role: `items`). Only `SegmentedControl.Item` children are read; any non-`Item` node (a `Fragment`, a wrapper, stray text) is a **dev-mode `console.error`** and is filtered in production. Each `Item`'s `value` is the collection identity (resolved through `shared/collection`) — duplicate `value`s are a dev-mode error (first wins in production); duplicate visible **labels** are always supported.
- `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.
- `defaultValue`: `string | undefined`; default `the first child's `value``. Uncontrolled seed for the internal selection (read once at mount; ignored once {@link SegmentedControlProps.value} is supplied). A seed that matches no `.Item` resolves to no selection, like a missing controlled `value`.
- `disabled`: `boolean | undefined`. When `true`, the whole control is dimmed, removed from the tab order (every segment `tabIndex={-1}`), and all interaction is suppressed. Since the root is a `<div role="radiogroup">` (no native `disabled`), this sets `aria-disabled` plus a `data-disabled` styling hook.
- `distribution`: `"fill" | "fit" | undefined`; default ``"fill"``. Width distribution — the `NSSegmentedControl.Distribution` analogue. `"fill"` (the default) makes the control **fill its container** (`display: grid`); `"fit"` makes it **hug its content** (`display: inline-grid`). Either way the segments stay equal-width; only container-fill vs content-hug differs. Resolved via the `data-distribution` attribute selector in `styles.css`.
- `labelProps`: `LabelProps | undefined`. Per-instance typography override for the segment 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 the segment's own selected/unselected colour (system-gray-1 unselected, primary selected). Applies only to string/number segment labels — an `<Icon>` segment passes through unwrapped.
- `onChange`: `((next: string) => void) | undefined`. Fires with the next selected item's `value` whenever the selection moves — pointer tap, drag-release, or a keyboard arrow / Home / End. Runs through the shared {@link useControllableState} triad, so it fires **at most once per accepted change** in both controlled and uncontrolled modes: a no-op re-selection, a disabled interaction, and a parent-driven controlled `value` change all emit nothing.
- `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`.
- `size`: `SegmentedControlSize | undefined`; default ``"regular"``. Size of the control — the full canonical {@link SegmentedControlSize} scale (`mini · small · regular · large · xlarge`). SegmentedControl is the exception that ships all five sizes (owner change). Resolved to geometry via the `data-size` attribute selector in `styles.css`; across the five sizes only the box scales — the label font-size stays constant.
- `value`: `string | undefined`. Controlled selected value. Passing this switches SegmentedControl to controlled mode: the rendered selection always reflects this value and the consumer owns it via {@link SegmentedControlProps.onChange}. A `value` that matches no `.Item` resolves to **no selection** (no segment `aria-checked`, the pill hidden at index 0, roving `tabIndex` on the first enabled segment) — it never silently selects index 0. Switching between controlled and uncontrolled after mount is a dev-mode `console.error`.

## SwiftUI mapping

- ``value`` → ``Picker(selection:)` get` (direct): — | controlled selection | string identity
- ``defaultValue`` → ``@State` initial selection` (web-only): first `.Item`'s `value` | uncontrolled seed | web-only idiom (ADR-0015)
- ``onChange`` → ``Picker(selection:)` set` (direct): — | change callback (both modes) | `(next: string) => void`
- ``children` (`.Item`)` → ``content:` `ForEach { … .tag(_:) }`` (direct): — | — | children role: `items`
- ``.Item.value`` → ``.tag(_:)`` (direct): — | identity | required, unique
- ``.Item.disabled`` → ``.disabled(_:)` on the tagged view` (direct): `false` | — | per-segment skip
- ``.Item.children`` → `the `Text`/`Image` label view` (direct): — | — | children role: `label`
- ``disabled`` → ``.disabled(_:)` on the `Picker`` (direct): `false` | — | Shape W (§3): `aria-disabled` + `data-disabled`
- ``size`` → ``.controlSize(_:)` (`ControlSize`)` (direct): `regular` | — | full 5-step (toolbar-density, §7)
- ``distribution`` → `AppKit `NSSegmentedControl.Distribution` (`.fill`/`.fit`)` (web-only): `fill` | — | no SwiftUI analogue; Apple-API pedigree (§5)
- ``labelProps`` → `label-view modifiers (`.font`/`.fontWeight`/`.foregroundStyle`)` (direct): — | — | ADR-0028 seam; text segments only
- ``className`` → `—` (web-only): — | — | web-only glass surface override
- ``ref`` → `—` (web-only): — | — | web-only; `Ref<HTMLDivElement>` (radiogroup)
- ``aria-label` / `aria-labelledby`` → ``.accessibilityLabel(_:)` / label view` (direct): — | — | required accessible name
