Skip to content
Dethink Components

ComponentsSlotPlanner

SlotPlanner

Manage available time slots and let users request bookings in their time zone.

On this page

Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.

View registry files
  • Internationalized Date (@internationalized/date)

    Date values, calendar arithmetic, and time-zone-aware date handling.

  • Motion for React (motion)

    Animations, transitions, and reduced-motion handling.

  • React Aria (react-aria) · via shared Dethink components

    Accessibility hooks and overlay utilities used by the implementation.

  • React Aria Components (react-aria-components) · via shared Dethink components

    Accessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.

Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.

Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.

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

Compare availability across the week. Select a slot to manage its day; narrow containers show a daily agenda.

Mentoring availability

America/New York

Sep 21 – 27

Tuesday, September 22, 2026

  • 09:00 – 09:45Available

    2 seats left

    45 minRecurring weeklyMentoring

    America/New_York

  • 14:00 – 14:30Request pending

    2 seats left

    30 min

    Group session — drop-in welcome.

    America/New_York

  • 16:00 – 17:00Booked

    0 seats left

    60 minMentoring

    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-09-22",
    startTime: "09:00",
    durationMinutes: 45,
    timeZone: "America/New_York",
    state: "requestable",
    capacity: 2,
    recurrence: { frequency: "weekly" },
    data: { tags: ["Mentoring"] },
  },
  {
    id: "mon-afternoon",
    date: "2026-09-22",
    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-09-21",
    startTime: "10:00",
    durationMinutes: 45,
    timeZone: "America/New_York",
    state: "requestable",
    data: { tags: ["Mentoring"] },
  },
  {
    id: "wed-review",
    date: "2026-09-23",
    startTime: "11:00",
    durationMinutes: 60,
    timeZone: "America/New_York",
    state: "requestable",
    bookedCount: 1,
    capacity: 1,
  },
  {
    id: "tue-booked",
    date: "2026-09-22",
    startTime: "16:00",
    durationMinutes: 60,
    timeZone: "America/New_York",
    state: "requestable",
    capacity: 1,
    bookedCount: 1,
    data: { tags: ["Mentoring"] },
  },
  {
    id: "thu-available",
    date: "2026-09-24",
    startTime: "13:00",
    durationMinutes: 30,
    timeZone: "America/New_York",
    state: "requestable",
    capacity: 2,
  },
];

export function SlotPlannerBasic() {
  return (
    <SlotPlanner
      title="Mentoring availability"
      defaultSlots={sampleSlots}
      defaultFocusedDate="2026-09-22"
      now="2026-09-21T08:00:00-04:00"
      timeZone="America/New_York"
      taxonomy={{
        statusLabels: {
          requestable: "Available",
          requested: "Request pending",
          booked: "Booked",
        },
        emptyDay: "No slots",
        addSlot: "Add slot",
      }}
    />
  );
}

Constraints and cap meter

A daily requestable cap, minimum notice, and a duration increment surfaced inline as the cap fills.

Advising hours

America/New York

Jul 6 – 12

Monday, July 6, 2026

Daily cap: 3 / 3 requestable slotsDaily cap reached

  • 09:00 – 09:30requested

    0 seats left

    30 min

    America/New_York

  • 11:00 – 11:30requested

    0 seats left

    30 min

    America/New_York

  • 15:00 – 15:30requested

    0 seats left

    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"
      timeZone="America/New_York"
      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

America/Los Angeles

Jul 6 – 12

Monday, July 6, 2026

  • 13:00 – 13:50requestable

    1 seat left

    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"
      timeZone="America/Los_Angeles"
      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

America/New York

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"
      timeZone="America/New_York"
      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>
        ),
      }}
    />
  );
}

Choose a date, then add or manage its slots.

The weekly calendar appears when the component has at least 620px of space. Select Saturday or Sunday to show the weekend. Select an appointment to open its day. The appointment menu contains Edit and Delete; Day actions contains copy and clear options. Choose Week to return to the calendar. Narrow containers show a date strip and daily agenda.

The calendar projects appointments into the time zone shown above it. The day editor uses the original appointment’s zone and date. Pass timeZone explicitly for consistent server and browser rendering. Slots crossing midnight appear on each affected day. Short slots have a minimum hit target and overlaps use separate lanes; the written times give the exact duration.

The editor includes duration presets and a custom duration field. More options contains capacity, buffers, time zone, tags, and notes. The end-time preview updates as you edit. New slots must meet the notice period and start in the future. Existing recurring appointments retain the choice between one occurrence and the whole series.

Use Tab to reach calendar controls and appointments, and Enter or Space to activate them. In the compact date strip, use arrow keys, Home, and End. Dialogs contain focus, close with Escape, and return focus to their trigger. Status labels communicate availability alongside colour. Motion follows reduced-motion preferences.

SlotPicker shows Request sent after its callback succeeds and prevents repeat activation for that occurrence while the picker stays mounted. This acknowledgement is local to the current picker. Your application owns authentication, persistent request state, seat enforcement, cancellation, and duplicate prevention across sessions.

Set weekLayout="agenda" to retain the earlier weekday-rail presentation. Providing custom renderers also retains that presentation, including the existing tab, list-item, and renderer semantics. The slot data model and mutation callback payloads are unchanged.

SlotPlanner (manage mode) and SlotPicker (book mode) share the same slot data model and taxonomy/motion/focus props.

SlotPlanner props
PropWhat it doesDefault
weekLayout"calendar" | "agenda"Calendar shows a time grid from 620px container width and a daily agenda below it. Agenda retains the original weekday rail. Custom renderers automatically use the agenda to preserve their structural contract."calendar"
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 for the weekly calendar, planner today, and new slots. Pass explicitly for deterministic renders and SSR. Existing slot edits retain their original zone.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