KQ-1740 · DURATION/SLOTS
Duration Slots
A public booking page with a host card, a length radiogroup, a time zone combobox, a month strip and the day's free times as pills. Changing the length from 30 to 60 minutes joins every pair of neighbouring pills with a liquid thread whose attach points spread and whose waist follows on springs.glide until the two are one tall pill, the times drifting together; changing back draws a seam across each pill and splits it on springs.snap. The day and length pills slide on springs.snap and a pick lands on springs.flick; the times are a listbox with arrow keys and the zone combobox has type-ahead.
Pick a time to book it.
30 min · 10 times on thu 22 oct
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/duration-slots
Props
- pillShape
- "round" | "soft" | "square"
- default "soft"
- The corners of every pill; the liquid join keeps them.
- density
- "compact" | "cozy" | "roomy"
- default "cozy"
- Pill height, gaps and type: 32, 38 or 44px pills.
- accent
- string
- default "var(--accent-bright)"
- Any CSS colour: the picked pill, the day and length pills, the free dots.
- availability
- Record<string, string[]>
- default defaultSlotAvailability
- Free slot starts per ISO date, "HH:MM" on the host's clock, one per slotMinutes.
- slotMinutes
- number
- default 30
- The grain of availability, minutes.
- durations
- number[]
- default [30, 60, 90]
- The lengths offered, each a multiple of slotMinutes.
- duration / defaultDuration
- number
- default — / the first of durations
- The length in minutes, controlled or initial.
- onDurationChange
- (minutes: number) => void
- default —
- Fires from the press or key that changed the length.
- date / defaultDate
- string
- default — / "2026-10-22"
- The day shown, ISO, controlled or initial.
- onDateChange
- (iso: string) => void
- default —
- Fires from the press or key that changed the day.
- value / defaultValue
- SlotPick | null
- default — / null
- The picked time { date, start, end } (minutes on the host's clock), controlled or initial.
- onPick
- (pick: SlotPick | null) => void
- default —
- A time was picked, or the pick was cleared by a change of day or length.
- onConfirm
- (pick: SlotPick) => void
- default —
- Confirm was pressed with the pick.
- zones
- SlotZone[]
- default defaultSlotZones
- Zones offered by the time zone box: { id, label, offset }.
- zone / defaultZone
- string
- default — / the host's zone
- The visitor's zone id, controlled or initial; every label shifts by the offset difference.
- onZoneChange
- (id: string) => void
- default —
- Fires when a zone is chosen.
- host
- SlotHost
- default defaultSlotHost
- Who is being booked: { name, role, place, about?, zone }.
- now
- number
- default defaultSlotNow
- The current time, ms; days before it are past. Never read from the clock.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the availability has arrived: loading shows faint pill outlines, error a Try again.
- onRetry
- () => void
- default —
- Try again was pressed after the availability failed to load.
- height
- number
- default 540
- Height of the page in px; its body scrolls inside.
- disabled
- boolean
- default false
- Nothing can be picked or confirmed.
- className
- string
- default —
- Classes for the outer surface.
| Prop | Type | Default | Description |
|---|---|---|---|
| pillShape | "round" | "soft" | "square" | "soft" | The corners of every pill; the liquid join keeps them. |
| density | "compact" | "cozy" | "roomy" | "cozy" | Pill height, gaps and type: 32, 38 or 44px pills. |
| accent | string | "var(--accent-bright)" | Any CSS colour: the picked pill, the day and length pills, the free dots. |
| availability | Record<string, string[]> | defaultSlotAvailability | Free slot starts per ISO date, "HH:MM" on the host's clock, one per slotMinutes. |
| slotMinutes | number | 30 | The grain of availability, minutes. |
| durations | number[] | [30, 60, 90] | The lengths offered, each a multiple of slotMinutes. |
| duration / defaultDuration | number | — / the first of durations | The length in minutes, controlled or initial. |
| onDurationChange | (minutes: number) => void | — | Fires from the press or key that changed the length. |
| date / defaultDate | string | — / "2026-10-22" | The day shown, ISO, controlled or initial. |
| onDateChange | (iso: string) => void | — | Fires from the press or key that changed the day. |
| value / defaultValue | SlotPick | null | — / null | The picked time { date, start, end } (minutes on the host's clock), controlled or initial. |
| onPick | (pick: SlotPick | null) => void | — | A time was picked, or the pick was cleared by a change of day or length. |
| onConfirm | (pick: SlotPick) => void | — | Confirm was pressed with the pick. |
| zones | SlotZone[] | defaultSlotZones | Zones offered by the time zone box: { id, label, offset }. |
| zone / defaultZone | string | — / the host's zone | The visitor's zone id, controlled or initial; every label shifts by the offset difference. |
| onZoneChange | (id: string) => void | — | Fires when a zone is chosen. |
| host | SlotHost | defaultSlotHost | Who is being booked: { name, role, place, about?, zone }. |
| now | number | defaultSlotNow | The current time, ms; days before it are past. Never read from the clock. |
| status | "ready" | "loading" | "error" | "ready" | Whether the availability has arrived: loading shows faint pill outlines, error a Try again. |
| onRetry | () => void | — | Try again was pressed after the availability failed to load. |
| height | number | 540 | Height of the page in px; its body scrolls inside. |
| disabled | boolean | false | Nothing can be picked or confirmed. |
| className | string | — | Classes for the outer surface. |