ComponentsSlotPlanner
SlotPlanner
Manage available time slots and let users request bookings in their time zone.
On this page
Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.
View registry filesImport the component into your page or component file.
import {
SlotPicker,
SlotPlanner,
useSlotPlanner,
validateSlotPlannerSlots,
type SlotPlannerConstraints,
type SlotPlannerSlotData,
} from "@dethink/components";SlotPlanner manages the provider-facing inventory; SlotPicker projects the same slots for booking.
Manage mode
Uncontrolled week view with mixed slot states, capacity, and a pending request.
Monday, July 6, 2026
- 09:00 โ 09:45requestable45 minRecurring weeklyOnboarding
America/New_York
- 14:00 โ 14:30requested30 min
Group session โ drop-in welcome.
America/New_York
Show sourceexamples/slot-planner/basic.tsx
"use client";
import { SlotPlanner, type SlotPlannerSlotData } from "@dethink/components";
const sampleSlots: SlotPlannerSlotData[] = [
{
id: "mon-morning",
date: "2026-07-06",
startTime: "09:00",
durationMinutes: 45,
timeZone: "America/New_York",
state: "requestable",
recurrence: { frequency: "weekly" },
data: { tags: ["Onboarding"] },
},
{
id: "mon-afternoon",
date: "2026-07-06",
startTime: "14:00",
durationMinutes: 30,
timeZone: "America/New_York",
state: "requestable",
capacity: 3,
requestedCount: 1,
data: { note: "Group session โ drop-in welcome." },
},
{
id: "tue-standup",
date: "2026-07-07",
startTime: "10:00",
durationMinutes: 15,
timeZone: "America/New_York",
state: "blocked",
data: { tags: ["Internal"] },
},
{
id: "wed-review",
date: "2026-07-08",
startTime: "11:30",
durationMinutes: 60,
timeZone: "America/New_York",
state: "requestable",
bookedCount: 1,
capacity: 1,
},
];
export function SlotPlannerBasic() {
return (
<SlotPlanner
title="Mentoring availability"
defaultSlots={sampleSlots}
defaultFocusedDate="2026-07-06"
now="2026-07-06T08:00:00-04:00"
/>
);
}Constraints and cap meter
A daily requestable cap, minimum notice, and a duration increment surfaced inline as the cap fills.
Monday, July 6, 2026
Daily cap: 3 / 3 requestable slotsDaily cap reached
- 09:00 โ 09:30requested30 min
America/New_York
- 11:00 โ 11:30requested30 min
America/New_York
- 15:00 โ 15:30requested30 min
America/New_York
Show sourceexamples/slot-planner/constraints.tsx
"use client";
import { SlotPlanner, type SlotPlannerSlotData } from "@dethink/components";
const sampleSlots: SlotPlannerSlotData[] = [
{
id: "mon-early",
date: "2026-07-06",
startTime: "09:00",
durationMinutes: 30,
timeZone: "America/New_York",
state: "requestable",
requestedCount: 1,
},
{
id: "mon-mid",
date: "2026-07-06",
startTime: "11:00",
durationMinutes: 30,
timeZone: "America/New_York",
state: "requestable",
requestedCount: 1,
},
{
id: "mon-late",
date: "2026-07-06",
startTime: "15:00",
durationMinutes: 30,
timeZone: "America/New_York",
state: "requestable",
requestedCount: 1,
},
];
export function SlotPlannerConstraints() {
return (
<SlotPlanner
title="Advising hours"
defaultSlots={sampleSlots}
defaultFocusedDate="2026-07-06"
now="2026-07-06T08:00:00-04:00"
constraints={{
dailyRequestableCap: 3,
minNoticeMinutes: 120,
durationIncrementMinutes: 15,
}}
/>
);
}Custom taxonomy
The same component phrased through a custom vocabulary โ "sessions" instead of "slots".
Monday, July 6, 2026
- 13:00 โ 13:50requestable50 minRecurring weeklyCBT
America/Los_Angeles
Show sourceexamples/slot-planner/taxonomy.tsx
"use client";
import { SlotPlanner, type SlotPlannerSlotData } from "@dethink/components";
const sampleSlots: SlotPlannerSlotData[] = [
{
id: "mon-session",
date: "2026-07-06",
startTime: "13:00",
durationMinutes: 50,
timeZone: "America/Los_Angeles",
state: "requestable",
recurrence: { frequency: "weekly" },
data: { tags: ["CBT"] },
},
{
id: "wed-session",
date: "2026-07-08",
startTime: "16:00",
durationMinutes: 50,
timeZone: "America/Los_Angeles",
state: "requestable",
data: { note: "Intake session for new clients." },
},
];
export function SlotPlannerTaxonomy() {
return (
<SlotPlanner
title="Therapy sessions"
defaultSlots={sampleSlots}
defaultFocusedDate="2026-07-06"
now="2026-07-06T09:00:00-07:00"
taxonomy={{
slot: "session",
slotPlural: "sessions",
addSlot: "Add session to this day",
requestSlot: "Request {slot}",
}}
/>
);
}Book mode with a distinct viewer zone
SlotPicker projects provider-zone slots into a different viewer time zone and reports requests without mutating the collection itself.
Monday, July 6, 2026
- 09:00 โ 09:30requestable
14:00 Europe/London
2 seats left
Show sourceexamples/slot-planner/book-mode.tsx
"use client";
import { useState } from "react";
import {
SlotPicker,
type SlotPlannerBookRequestPayload,
type SlotPlannerSlotData,
} from "@dethink/components";
const initialSlots: SlotPlannerSlotData[] = [
{
id: "mon-consult",
date: "2026-07-06",
startTime: "14:00",
durationMinutes: 30,
timeZone: "Europe/London",
state: "requestable",
recurrence: { frequency: "weekly" },
capacity: 2,
},
{
id: "tue-consult",
date: "2026-07-07",
startTime: "17:30",
durationMinutes: 30,
timeZone: "Europe/London",
state: "requestable",
capacity: 1,
},
];
export function SlotPlannerBookMode() {
const [slots, setSlots] = useState(initialSlots);
const handleBookRequest = (payload: SlotPlannerBookRequestPayload) => {
setSlots((previous) =>
previous.map((slot) => {
if (slot.id !== payload.slotId) {
return slot;
}
if (!slot.recurrence || payload.occurrenceDate === slot.date) {
return { ...slot, requestedCount: (slot.requestedCount ?? 0) + 1 };
}
const overrides = slot.recurrence.overrides ?? [];
const existing = overrides.find(
(override) => override.occurrenceDate === payload.occurrenceDate,
);
const nextOverride = {
...existing,
occurrenceDate: payload.occurrenceDate,
requestedCount: (existing?.requestedCount ?? 0) + 1,
};
return {
...slot,
recurrence: {
...slot.recurrence,
overrides: existing
? overrides.map((override) =>
override.occurrenceDate === payload.occurrenceDate
? nextOverride
: override,
)
: [...overrides, nextOverride],
},
};
}),
);
};
return (
<SlotPicker
title="Book a consultation"
slots={slots}
viewerTimeZone="America/New_York"
defaultFocusedDate="2026-07-06"
now="2026-07-06T08:00:00-04:00"
onBookRequest={handleBookRequest}
/>
);
}Custom slot card renderer
Every surface exposes renderDefault() so a custom renderer can decorate the shipped card instead of rebuilding it.
Monday, July 6, 2026
- 10:00 โ 10:45requestable45 minRecurring weekly
America/New_York
$120 per seat
- 15:00 โ 16:30requestable90 min
America/New_York
$45 per seat
Show sourceexamples/slot-planner/custom-renderer.tsx
"use client";
import { SlotPlanner, type SlotPlannerSlotData } from "@dethink/components";
const sampleSlots: SlotPlannerSlotData<{ priceUsd: number }>[] = [
{
id: "mon-consult",
date: "2026-07-06",
startTime: "10:00",
durationMinutes: 45,
timeZone: "America/New_York",
state: "requestable",
recurrence: { frequency: "weekly" },
data: { priceUsd: 120 },
},
{
id: "mon-workshop",
date: "2026-07-06",
startTime: "15:00",
durationMinutes: 90,
timeZone: "America/New_York",
state: "requestable",
capacity: 6,
data: { priceUsd: 45 },
},
];
export function SlotPlannerCustomRenderer() {
return (
<SlotPlanner
title="Paid consultations"
defaultSlots={sampleSlots}
defaultFocusedDate="2026-07-06"
now="2026-07-06T08:00:00-04:00"
renderers={{
slotCard: ({ occurrence, renderDefault }) => (
<div className="grid gap-2">
{renderDefault()}
{typeof occurrence.slot.data?.priceUsd === "number" ? (
<p className="text-foreground text-sm font-medium">
${occurrence.slot.data.priceUsd} per seat
</p>
) : null}
</div>
),
}}
/>
);
}SlotPlanner (manage mode) and SlotPicker (book mode) share the same slot data model and taxonomy/motion/focus props.
| Prop | What it does | Default |
|---|---|---|
slots / defaultSlotsSlotPlannerSlotData[] | Controlled or uncontrolled slot collection. Recurring slots repeat weekly or biweekly with per-occurrence overrides. | uncontrolled |
focusedDate / defaultFocusedDate / onFocusedDateChangestring / string / (dateIso) => void | Controls the focused ISO date (`YYYY-MM-DD`) driving the visible week and selected day. | uncontrolled / today / undefined |
view / defaultView / onViewChange"week" | "day" / "week" | "day" / (view) => void | Controls the visible projection. Day view hides the rail while keeping toolbar navigation and the view switcher. | uncontrolled / "week" / undefined |
timeZonestring | IANA zone used to derive planner today and default editor values. Pass explicitly for deterministic renders and SSR. | environment zone |
constraintsSlotPlannerConstraints | Declarative rules โ daily/weekly caps, duration bounds, notice period, booking horizon, blackout dates, working days โ validated on editor saves and batch operations, and surfaced by the cap meter. | undefined |
taxonomySlotPlannerTaxonomyInput | Overrides any subset of the noun, verb, and announcement vocabulary, e.g. renaming "slot" to "session" or "appointment". | neutral slot language |
onCreateSlot / onUpdateSlot(payload) => void | Promise<void> | Mutation callbacks fired from the editor dialog. A returned promise drives per-key pending/error/retry affordances. | undefined |
onDeleteOccurrence / onDeleteSeries(payload) => void | Promise<void> | Fired for single-occurrence and whole-series deletion, after the structural delete confirm dialog. | undefined |
onBatchChange(payload: SlotPlannerBatchChangePayload) => void | Promise<void> | Fired for copy-day, copy-week, and clear-day operations with created/deleted/updated slots and per-slot violations. | undefined |
now / localestring / string | Injectable "now" instant and locale for deterministic renders, including SSR. | current time / environment locale |
loading / errorboolean / ReactNode | External async states for app-owned fetching. The day panel shows status text and hides mutation affordances while active. | false / undefined |
titleReactNode | Heading rendered above the toolbar. | undefined |
reducedMotionboolean | Forces the reduced-motion rendering path; every animation collapses to an instant state change either way. | prefers-reduced-motion |
renderersSlotPlannerRenderers | Render props for the toolbar, day card, day header, cap meter, empty day, and slot card, each with a renderDefault() escape hatch for decoration. | undefined |
| Prop | What it does | Default |
|---|---|---|
slotsSlotPlannerSlotData[] | Read-only slot collection to browse. SlotPicker never mutates it โ a request only fires onBookRequest. | required |
viewerTimeZonestring | IANA zone the occurrences are projected into for display and day grouping. Pass explicitly for deterministic renders and SSR. | environment zone |
onBookRequest(payload: SlotPlannerBookRequestPayload) => void | Promise<void> | Fires when the viewer requests an available occurrence. occurrenceDate stays the provider-zone date; a returned promise drives pending/error/retry. | undefined |
taxonomySlotPlannerTaxonomyInput | Same vocabulary override as SlotPlanner, phrased for book mode. | neutral slot language |
view / defaultView / onViewChange"week" | "day" / "week" | "day" / (view) => void | Controls the visible projection. Day view hides the rail while keeping toolbar navigation and the view switcher. | uncontrolled / "week" / undefined |
focusedDate / defaultFocusedDate / onFocusedDateChangestring / string / (dateIso) => void | Controls the focused viewer-zone ISO date driving the visible week and selected day. | uncontrolled / viewer-zone today / undefined |
now / localestring / string | Injectable "now" instant and locale for deterministic renders. | current time / environment locale |
loading / errorboolean / ReactNode | External async states for app-owned fetching. The day panel shows status text and hides the slot list while active. | false / undefined |
titleReactNode | Heading rendered above the toolbar. | undefined |
reducedMotionboolean | Forces the reduced-motion rendering path. | prefers-reduced-motion |
renderersSlotPickerRenderers | Render props for the slot card and empty day, each with a renderDefault() escape hatch for decoration. | undefined |