# Stepper

**Maturity:** experimental

A user-tappable control for incrementing or decrementing a numeric value by a fixed step — the canonical glass stepper.

## Import

```tsx
import { Stepper } from "@liquidify/react/stepper"
```

## Props

- `children`: `ReactNode`. Optional visible label content, rendered next to the capsule in a row — the SwiftUI `label:` closure / `init(_ titleKey:)` analogue (`<Stepper aria-label="Quantity">Quantity</Stepper>`). The accessible name is **not** sourced from here; supply `aria-label` / `aria-labelledby` for the screen-reader name (the segment buttons derive theirs from it). children role: `label`.
- `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`: `number | undefined`; default ``min` (when bounded), else `0``. Uncontrolled seed for the internal value (ignored once {@link StepperValueProps.value} is supplied). Clamped into `[min, max]`. Absent in callback mode — no value binding exists.
- `disabled`: `boolean | undefined`. When `true`, both segments are inert; `aria-disabled` + `data-disabled` on the root; `disabled` on each segment button; `tabIndex={-1}` on the root.
- `labelProps`: `LabelProps | undefined`. Per-instance typography override for the visible label, forwarded to the internal {@link Text} (ADR-0028). Token-backed axes only (`variant`, `weight`, `color`, `tint`, `tracking`, …); the HIG default is `variant="body"` / `color="primary"`. Applies only when {@link StepperCommonProps.children} is a string/number — a label element the consumer passes is rendered as-is.
- `max`: `number | undefined`; default ``undefined` (unbounded)`. Upper bound of the value range. Absent in callback mode — there is no range to bound.
- `min`: `number | undefined`; default ``undefined` (unbounded)`. Lower bound of the value range. Absent in callback mode — there is no range to bound.
- `onChange`: `((next: number) => void) | undefined`. Fires with the next clamped value whenever the value changes. Called in both controlled and uncontrolled modes. Absent in callback mode — no value binding exists.
- `onDecrement`: `(() => void) | undefined`. See {@link StepperValueProps.onIncrement}. Fires on each `−` press.
- `onEditingChanged`: `((editing: boolean) => void) | undefined`. Fires `true` on segment `pointerdown`, `false` on `pointerup` / `pointercancel` — the SwiftUI `onEditingChanged` analogue.
- `onIncrement`: `(() => void) | undefined`. Not on the value branch — callback-only mode is mutually exclusive with a value binding (charter §6). Fires on each `+` press.
- `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`.
- `step`: `number | undefined`; default ``1``. Increment / decrement magnitude. A zero, `NaN`, non-finite, or negative `step` coerces to the default `1` (a negative `step` no longer reverses direction). Absent in callback mode — there is no value to step.
- `value`: `number | undefined`. Controlled value. Passing this switches Stepper to controlled mode: the rendered value always reflects this (clamped) and the consumer owns it via {@link StepperValueProps.onChange}. Absent in callback mode — no value binding exists.

## SwiftUI mapping

- ``value: Binding<V>`` → ``value` (controlled) / `defaultValue` (uncontrolled seed)` (direct): The controlled-numeric triad (ADR-0015), on the `StepperValueProps` arm. A `Binding` splits into controlled `value` + `onChange`, or uncontrolled `defaultValue`. Presence selects the value arm — the default when neither `onIncrement` nor `onDecrement` is supplied.
- ``in: ClosedRange<V>`` → ``min` / `max`` (direct): Inclusive range bounds, `StepperValueProps` only. Demo uses `0...10`, `0...5`. Omitting both defaults to unbounded (`-Infinity` / `Infinity`).
- ``step: V.Stride`` → ``step`` (direct): Increment/decrement magnitude, `StepperValueProps` only. SwiftUI default `1`; demo never sets it explicitly. React default `1` — a zero, `NaN`, non-finite, or **negative** `step` also coerces to `1` (a negative step no longer reverses direction).
- ``init(_ titleKey:)` / `label: () -> Label`` → ``children` (ReactNode / string)` (direct): Leading visible label content rendered next to the capsule **through the `Text` primitive** (default `variant="body"` / `color="primary"`) when `children` is a string/number; a label *element* the consumer passes is rendered as-is (no double-wrap, ADR-0028). The accessible name is sourced from `aria-label` / `aria-labelledby`, not `children`. Demo: `"Quantity: \(qty)"`, `Text("Bounded 0–10")`, `"Custom actions"`.
- ``.font(_:)` / `.foregroundStyle(_:)` on the label` → ``labelProps` (`Pick<TextProps, …>`)` (direct): Per-instance typography override forwarded to the internal `Text` (token-backed axes only — `variant`/`weight`/`color`/`tint`/`tracking`/…; ADR-0028). Spread last, so an explicit value wins over the `body`/`primary` default. Applies only to string/number `children`.
- ``onIncrement: (() -> Void)?`` → ``onIncrement?`` (direct): Callback mode (`StepperCallbackProps`), the discriminant. Fires on each `+` press.
- ``onDecrement: (() -> Void)?`` → ``onDecrement?`` (direct): Callback mode (`StepperCallbackProps`), the discriminant. Fires on each `−` press.
- ``onEditingChanged: (Bool) -> Void`` → ``onEditingChanged?(editing: boolean)`` (direct): Fires `true` on segment press-start, `false` on release. Optional; shared by both arms; mirrors Slider's adoption of this pattern.
- ``.disabled(true)`` → ``disabled` (from `ControlProps`)` (direct): Whole-control dim; both segments inert. Shared by both arms.
- ``format:` (FormatStyle variants)` → `(out of scope v1)` (direct): Deferred number-formatting forms.
- ``.tint(_:)`` → `(not present)` (direct): No tint axis for Stepper (demo renders no accent).
