Skip to content
Dethink Components

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 files

Import the component into your page or component file.

Usage
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.

Mentoring availability

Monday, July 6, 2026

  • 09:00 โ€“ 09:45requestable
    45 minRecurring weeklyOnboarding

    America/New_York

  • 14:00 โ€“ 14:30requested
    30 min

    Group session โ€” drop-in welcome.

    America/New_York

Show sourceexamples/slot-planner/basic.tsx
examples/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.

Advising hours

Monday, July 6, 2026

Daily cap: 3 / 3 requestable slotsDaily cap reached

  • 09:00 โ€“ 09:30requested
    30 min

    America/New_York

  • 11:00 โ€“ 11:30requested
    30 min

    America/New_York

  • 15:00 โ€“ 15:30requested
    30 min

    America/New_York

Show sourceexamples/slot-planner/constraints.tsx
examples/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".

Therapy sessions

Monday, July 6, 2026

  • 13:00 โ€“ 13:50requestable
    50 minRecurring weeklyCBT

    America/Los_Angeles

Show sourceexamples/slot-planner/taxonomy.tsx
examples/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.

Book a consultation

Monday, July 6, 2026

  • 09:00 โ€“ 09:30requestable

    14:00 Europe/London

    2 seats left

Show sourceexamples/slot-planner/book-mode.tsx
examples/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.

Paid consultations

Monday, July 6, 2026

  • 10:00 โ€“ 10:45requestable
    45 minRecurring weekly

    America/New_York

    $120 per seat

  • 15:00 โ€“ 16:30requestable
    90 min

    America/New_York

    $45 per seat

Show sourceexamples/slot-planner/custom-renderer.tsx
examples/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.

SlotPlanner props
PropWhat it doesDefault
slots / defaultSlotsSlotPlannerSlotData[]Controlled or uncontrolled slot collection. Recurring slots repeat weekly or biweekly with per-occurrence overrides.uncontrolled
focusedDate / defaultFocusedDate / onFocusedDateChangestring / string / (dateIso) => voidControls 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) => voidControls the visible projection. Day view hides the rail while keeping toolbar navigation and the view switcher.uncontrolled / "week" / undefined
timeZonestringIANA zone used to derive planner today and default editor values. Pass explicitly for deterministic renders and SSR.environment zone
constraintsSlotPlannerConstraintsDeclarative 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
taxonomySlotPlannerTaxonomyInputOverrides 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 / stringInjectable "now" instant and locale for deterministic renders, including SSR.current time / environment locale
loading / errorboolean / ReactNodeExternal async states for app-owned fetching. The day panel shows status text and hides mutation affordances while active.false / undefined
titleReactNodeHeading rendered above the toolbar.undefined
reducedMotionbooleanForces the reduced-motion rendering path; every animation collapses to an instant state change either way.prefers-reduced-motion
renderersSlotPlannerRenderersRender props for the toolbar, day card, day header, cap meter, empty day, and slot card, each with a renderDefault() escape hatch for decoration.undefined
SlotPicker props
PropWhat it doesDefault
slotsSlotPlannerSlotData[]Read-only slot collection to browse. SlotPicker never mutates it โ€” a request only fires onBookRequest.required
viewerTimeZonestringIANA 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
taxonomySlotPlannerTaxonomyInputSame vocabulary override as SlotPlanner, phrased for book mode.neutral slot language
view / defaultView / onViewChange"week" | "day" / "week" | "day" / (view) => voidControls the visible projection. Day view hides the rail while keeping toolbar navigation and the view switcher.uncontrolled / "week" / undefined
focusedDate / defaultFocusedDate / onFocusedDateChangestring / string / (dateIso) => voidControls the focused viewer-zone ISO date driving the visible week and selected day.uncontrolled / viewer-zone today / undefined
now / localestring / stringInjectable "now" instant and locale for deterministic renders.current time / environment locale
loading / errorboolean / ReactNodeExternal async states for app-owned fetching. The day panel shows status text and hides the slot list while active.false / undefined
titleReactNodeHeading rendered above the toolbar.undefined
reducedMotionbooleanForces the reduced-motion rendering path.prefers-reduced-motion
renderersSlotPickerRenderersRender props for the slot card and empty day, each with a renderDefault() escape hatch for decoration.undefined