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.

KQ-1740 · DURATION/SLOTS

Coldbrook Clinic

Consultation with Hana Ito

Registered dietitian

Length

October 2026

Thursday 22 October

10 times · 30 min · Lisbon time

Morning

09:00 to 09:30, Lisbon time
09:30 to 10:00, Lisbon time
10:00 to 10:30, Lisbon time
11:00 to 11:30, Lisbon time
11:30 to 12:00, Lisbon time

Afternoon

13:30 to 14:00, Lisbon time
14:00 to 14:30, Lisbon time
14:30 to 15:00, Lisbon time
15:00 to 15:30, Lisbon time
16:30 to 17:00, Lisbon time

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.