import type { IntentName } from "@charcuterie/tokens"; import type { ComponentPropsWithRef, ReactNode } from "react"; import type { SliderSize } from "../sliderStyles.ts"; import type { RangeSliderValue } from "./rangeSliderValue.ts"; /** * A position along the range, drawn as a mark on the bar. * * Prop-driven and nothing more: a chapter list, a scene index, a set * of preset prices. The component holds no model of what a tick * means, and `label` is optional because a mark that needs no words * is the common case. */ export type RangeSliderTick = { label?: string; value: number; }; export type RangeSliderProps = Omit, "children" | "defaultValue" | "onChange"> & { intent?: IntentName; isDisabled?: boolean; isLabelVisible?: boolean; /** * Read-only in the `aria-readonly` sense: focusable, full contrast, * announced as unchangeable — the same distinction `Slider` draws. * A clip already sent to a renderer is this, not `isDisabled`: the * span is still worth reading. */ isReadOnly?: boolean; isValueShown?: boolean; /** Required. Each thumb's own name is built from it. */ label: string; /** PageUp / PageDown. Defaults to a tenth of the range, as `Slider`'s does. */ largeStep?: number; max?: number; min?: number; /** * Fires on every movement — each arrow key, and each pointer sample * while dragging. Use it to paint. See `onChangeEnd` for committing. */ onChange?: (value: RangeSliderValue) => void; /** * Fires once, when the interaction ends: pointer release, or key up. * * The same split `Slider` settled, for the same reason. A clip * preview that re-seeks a player from `onChange` issues one seek per * pointer sample and arrives at the right frame **last**, behind a * queue of stale ones. */ onChangeEnd?: (value: RangeSliderValue) => void; size?: SliderSize; step?: number; /** * Marks along the bar. Positions in the same units as `value`, so a * film's chapters are the offsets the app already has. * * A `label` is centred on its own mark and clamped inside the bar at * both ends. Two marks closer together than a label box will overlap: * the component draws what it is given, and a caller with dense marks * passes the words only where there is room for them. */ ticks?: readonly RangeSliderTick[]; /** * Seeds the range; it does not control it. A drag paints from the * component's own value, so a caller that commits on `onChangeEnd` * alone still sees the thumbs follow the pointer. */ value?: RangeSliderValue; /** * How each end reads aloud and, when `isValueShown`, on screen. * This is what keeps timecodes out of the library: the component * knows a number, the app knows it is 21:14. */ valueFormat?: (value: number) => string; }; /** * A two-thumb range control: a span picked out along a bar, with an * independently focusable handle at each end. * * It exists because QueuePilot lets a user play **only a section** of * a video, and a clip's start and end are a shape `Slider` states it * is not: *"a two-thumb range is a different widget with its own * focus model"* * ([decision](../../../../docs/decisions/2026-09-01-a-two-thumb-range-is-its-own-component-and-shares-the-bar.md)). * * ## Why this is a component and not `Slider isRange` * * `Combobox`'s `isMultiple` and `DatePicker`'s `isRange` are modes * because neither changes the widget's role and both share nearly * everything. Both halves of that test fail here. * * `Slider` puts `role="slider"`, the tab stop and the pointer target * **on the track**, because one track reports one value. Two values * cannot live on one accessible object, so here the role moves to the * thumbs, the track becomes a `role="group"` carrying the label, and * the tab stops go from one to two. A mode that rewrote its own role * would break `getByRole("slider", { name })` in every consumer that * had ever used the single-value one. * * What the two genuinely share is the arithmetic * (`Slider/sliderValue.ts`, called rather than copied) and the bar * (`../sliderStyles.ts`) — so a range slider snaps on the same grid * and paints as the same control. * * ## The thumbs clamp, they do not swap * * A thumb dragged past its partner stops on it and the range collapses * to zero width. The thumbs never trade identities mid-drag: the * handle under the pointer is the one the user grabbed, and the * focused handle is the one an arrow key moves. Each thumb reports the * other as its own `aria-valuemin` / `aria-valuemax`, which is how a * screen reader is told where this handle stops. * * A press on the bar moves the **nearer** thumb. When the two sit on * the same value, the press's side decides — which is what lets a * collapsed range be pulled open again in either direction. */ export declare const RangeSlider: ({ className, intent, isDisabled, isLabelVisible, isReadOnly, isValueShown, label, largeStep, max, min, onChange, onChangeEnd, size, step, ticks, value, valueFormat, ...divProps }: RangeSliderProps) => ReactNode; //# sourceMappingURL=RangeSlider.d.ts.map