# TimeRangePicker
`TimeRangeField` + `Popover` with per-endpoint sliders. The segmented inputs accept keyboard editing; the popover exposes drag sliders (hour / minute / second) plus an AM/PM toggle for each endpoint (start and end). Picker owns the shared reactive state; `TimeRangeField` and `Popover` read the same `writableActive` refs.
## Anatomy
```svelte
Meeting window
{#snippet children({ segments })}
{#each segments as segment, i (i)}
{segment.value}
{/each}
{/snippet}
β
{#snippet children({ segments })}
{#each segments as segment, i (i)}
{segment.value}
{/each}
{/snippet}
π
β¦
β¦
β¦
β¦
β¦
```
The per-endpoint slider / toggle wrappers read their endpoint from the required `type: 'start' | 'end'` prop and drive the same underlying `{ start, end }` state as the segmented input.
## Parts
| Part | Source | Description |
| ------------------ | ------------------- | ----------------------------------------------------------------------- |
| `Provider` | range picker | Shared `value: TimeRange` + placeholder + open. Creates Popover + TRF. |
| `Trigger` | range picker | Popover trigger button. |
| `HourSlider` | range picker | Per-endpoint hour slider (`type: 'start' \| 'end'`). |
| `MinuteSlider` | range picker | Per-endpoint minute slider. |
| `SecondSlider` | range picker | Per-endpoint second slider. |
| `DayPeriodToggle` | range picker | Per-endpoint AM/PM radio group (rendered only in 12-hour cycle). |
| `Label` | TimeRangeField | Re-export. Single label naming the whole range. |
| `Input` | TimeRangeField | Re-export. One endpoint's segmented input (`type: 'start' \| 'end'`). |
| `Segment` | TimeField | Re-export. Resolves to the nearest Input's TimeField ctx. |
| `Content` | Popover | Re-export. Surface mounted via floating layer. |
| `Arrow` | Popover | Re-export. |
| `Close` | Popover | Re-export. |
| `Overlay` | Popover | Re-export. |
| `Anchor` | Popover | Re-export. |
| `Thumb` | Slider | Re-export. Used inside each slider wrapper. |
| `Range` | Slider | Re-export. Used inside each slider wrapper. |
| `Tick` | Slider | Re-export. Optional tick marks. |
## ARIA
- Sliders emit `role="slider"` with per-endpoint `aria-label` resolved through UIX lang refs (`morfo.langs` for component-owned labels, `langs.ts` only for legacy imperative constants).
- `DayPeriodToggle` emits `role="radiogroup"` with two `` items.
- Input / Segment inherit TimeRangeField / TimeField ARIA.
## Data Attributes
| Part | Attribute | Values |
| ----------------- | --------------------------------- | ---------------------------------------- |
| Root | `data-time-range-picker` | Always present |
| Trigger | `data-time-range-picker-trigger` | Always present |
| Trigger | `data-state` | `open` \| `closed` |
| Sliders | `data-endpoint` | `start` \| `end` |
| DayPeriodToggle | `data-endpoint` | `start` \| `end` |
| DayPeriodToggle | `data-time-range-picker-day-period` | Always present |
| Input | + all `data-time-range-field-*` | Inherited from TimeRangeField |
| Segment | + all `data-time-field-segment-*` | Inherited from TimeField |
| Content / Arrow / Close / Overlay / Anchor | + all `data-popover-*` | Inherited from Popover |
## Keyboard
- **Input segments**: identical to TimeRangeField β arrows cycle, digits auto-advance, Home/End, Tab between segments, Backspace clears, `A`/`P` toggles AM/PM.
- **Trigger**: Enter/Space opens the popover.
- **Sliders**: Arrow keys step by `{hour,minute,second}Step`, Home/End jump to min/max.
- **DayPeriodToggle**: Arrow keys / Space toggle between AM and PM.
- **Popover**: Escape closes; focus returns to the trigger (non-modal via FocusScope).
## Props
See `types.ts` for the JSDoc-documented surface. Summary:
| Prop | Type | Notes |
| ----------------------------------------- | -------------------------------- | ---------------------------------------------------------- |
| `value` | `TimeRange` | Bindable `{ start, end }`. |
| `onValueChange` | `(v) => void` | Fires when both endpoints resolve. |
| `onStartValueChange` / `onEndValueChange` | `(v) => void` | Every endpoint change (even incomplete). |
| `placeholder` | `TimeValue` | Bindable. Drives format for both inputs. |
| `open` | `boolean` | Bindable. Popover open state. |
| `onOpenChange` / `onOpenChangeComplete` | `(open) => void` | Change callbacks + post-animation. |
| `closeOnRangeComplete` | `boolean` | Auto-close when both endpoints set. @default false |
| `validate` | `TimeRangeValidator` | Receives `{ start, end }` β return error or β
. |
| `minValue` / `maxValue` | `TimeValue` | Applied to both endpoints. |
| `disabled` / `readonly` / `required` | `boolean` | Merged with parent Field if present. |
| `startReadonlySegments` | `EditableTimeSegmentPart[]` | Per-endpoint locked segments on start (A25). |
| `endReadonlySegments` | `EditableTimeSegmentPart[]` | Per-endpoint locked segments on end (A25). |
| `granularity` | `'hour' \| 'minute' \| 'second'` | Drives segment set for both inputs. |
| `hourCycle` | `12 \| 24` | Numeric. Falls back to locale. |
| `hideTimeZone` | `boolean` | Hide tz segment on ZonedDateTime. |
| `hourStep` / `minuteStep` / `secondStep` | `number` | Slider step sizes. @default 1 |
| `locale` / `dir` | `string` / `'ltr' \| 'rtl'` | Inherited from Soma when omitted. |
| `errorMessageId` | `string` | External error element id (aria-describedby). |
### Field integration
Wrapping in `Field.Provider` inherits `disabled`/`readonly`/`required`/`invalid` into the picker (OR-merged). Labels/helper-text from Field wire normally.
## Validation
Range validation runs on the provider when both endpoints are set and fires `onInvalid` with one of:
| Reason | Condition |
| --------- | -------------------------------------------- |
| `min` | `start < minValue` |
| `max` | `end > maxValue` or `start > maxValue` |
| `invalid` | `end < start` (natural order) |
| `custom` | `validate({start,end})` returned a message |
## Comparison with reference libraries
| Feature | React Aria | Ark UI | Bits UI | **Soma** |
| ------------------------------------------ | -------------- | -------------- | -------------- | ------------------------------------- |
| Dedicated time-range picker | β | β | β | β **Soma innovation** |
| Paired segmented inputs | β (in Dateα΅£) | β (in Dateα΅£) | β (in Dateα΅£) | β |
| Per-endpoint popover sliders | β | β | β | β |
| `closeOnRangeComplete` | β | `closeOnComplete` (DateRangePicker) | β | β |
| `onStartValueChange` / `onEndValueChange` | β (in Dateα΅£) | β | β | β |
| Range-level `validate` | β (in Dateα΅£) | β | β | β |
| Natural order check (`end β₯ start`) | β | β | β | β |
| Per-endpoint `readonlySegments` | β | β | β | β (A25) |
| `hh`/`mm`/`ss` placeholders | β | β | β | β (A28) |
| `beforeinput` blocks IME/paste | β | β | β | β (A26) |
| Shared state via single composed provider | β | β | β | β (A27) |
| Parent Field integration | via Form | via FieldRoot | β | β (soma Field) |
| Anatomy | β | β | β | 17 parts (6 unique + 11 re-exports) |
No mainstream library ships a dedicated time-range picker β they collapse it into a date-range picker via `CalendarDateTime`. Soma keeps them separate so pure time ranges (office hours, meeting windows) do not force a date.
## Example β meeting scheduler
```svelte
Availability
{#snippet children({ segments })}
{#each segments as segment, i (i)}
{segment.value}
{/each}
{/snippet}
{#snippet children({ segments })}
{#each segments as segment, i (i)}
{segment.value}
{/each}
{/snippet}
β°
```