# Appointment Scheduler

> Appointment booking for React and shadcn: pick a provider, day and time, and the chosen slot lifts into the confirmation card.

Book a visit in three choices: provider and day highlights glide, the day's slots re-tint in a ripple, and the picked time lifts out of the grid into the confirmation card before Book morphs to Booked.

- Tier: Free (MIT)
- Interaction: Press
- Page: https://eased.dev/components/appointment-scheduler
- Installs: motion
- Registry: https://eased.dev/r/appointment-scheduler.json

## Install

```bash
npx shadcn add https://eased.dev/r/appointment-scheduler.json
```

Booking a visit in three choices: a provider, a day and a time, ending in a confirmation card. Built on Avatar, Button and the shared Indicator, with the availability maths inline.

## When to use

- A patient portal or clinic site where people book a fixed-length visit with one of a few clinicians.
- When open and taken times should both be visible, so the grid never changes shape between days.

## When not to use

- Free-form calendars with drag to create: build on calendar-core's `timeGrid`.
- Picking a date with no times: use [Date Picker](/components/date-picker).

## Usage

```tsx example.tsx
import { AppointmentScheduler } from "@/components/eased/appointment-scheduler/appointment-scheduler";

export function Book({ providers, busy, days }: Data) {
  return (
    <AppointmentScheduler
      title="Book a visit"
      providers={providers}
      busy={busy}
      days={days}
      hours={{ start: "08:30", end: "16:30", lunch: ["12:00", "13:00"], slotMinutes: 30 }}
      visit={{ label: "Follow-up", minutes: 20 }}
      onBook={(booking) => api.book(booking)}
    />
  );
}
```

## API reference

### AppointmentScheduler

| Prop | Type | Default | Description |
|---|---|---|---|
| `providers` | `{ id, name, short, role? }[]` | | `short` labels the chip ("Dr. Achebe"). |
| `busy` | `Record<string, { start, end }[]>` | | Booked time per provider (epoch ms). |
| `days` | `number[]` | | Bookable days at midnight in `timeZone`. |
| `hours` | `{ start, end, lunch?, slotMinutes }` | | Clinic hours as `"HH:MM"`. |
| `visit` | `{ label, minutes }` | | A slot is open when the whole visit fits. |
| `onBook` | `(booking) => void \| Promise<void>` | | Book spins until it settles, then shows Booked. |
| `defaultProvider` | `string` | first | |
| `title` | `ReactNode` | | Leading side of the toolbar. |
| `locale` / `timeZone` | `string` | `"en-US"` / `"UTC"` | |
| `className` | `string` | `""` | |

## Keyboard

| Key | Action |
|---|---|
| `Tab` | Moves between the provider group, the day group, the open times and Book. |
| `←` / `→` | Move within the provider and day groups. |
| `Enter` / `Space` | Pick a time; press Book. |

## Accessibility

- Providers and days are radio groups; days are named with their full date and open count.
- Taken times stay in place as disabled buttons named "taken"; your booking is named "booked for you".
- Book stays focusable while disabled, and keeps focus while it changes to Booked.

## Motion

Provider and day highlights glide on the snappy spring. Changing either re-tints the time grid in a 12ms ripple. Picking a time flies a copy of the slot into the card on `smooth`, with a small lift, and the end time fades in once it lands; a new pick mid-flight replaces the flight. Book shows a spinner, then the label morphs to Booked as the check draws. Reduced motion: no flight or ripple.

## Notes for AI

Pick this for booking a fixed-length visit against known availability. Pass busy intervals, not open slots; the block derives open times and keeps taken ones visible.

Don't hide taken times or reflow the grid per day; the fixed grid is the point. Don't send personal health details in `onBook`'s URL or query string.

## Related

- [Date Picker](/components/date-picker): a date on its own.
- [Care Team](/components/care-team): who the patient can book or message.

## Example

`components/eased/appointment-scheduler/appointment-scheduler-demo.tsx`

```tsx
"use client";

import { clinicHours, providerBusy, providers, visitTypes } from "../_primitives/fixtures/health";
import { daysFromNow } from "../_primitives/fixtures/shared";
import { DemoFrame } from "../lib/demo-frame";
import { AppointmentScheduler } from "./appointment-scheduler";

const BOOKABLE = providers.filter((p) => p.id in providerBusy);
/** Mon 19 – Fri 23 Oct at midnight, clinic time. */
const DAYS = [1, 2, 3, 4, 5].map((d) => daysFromNow(d, 0, 0));

/** Booking Elena's lipid follow-up at Willow Health: pick a clinician, a day and a time. */
export function AppointmentSchedulerDemo() {
  return (
    <DemoFrame>
      <AppointmentScheduler
        className="flex-1"
        title="Book a visit"
        providers={BOOKABLE}
        busy={providerBusy}
        days={DAYS}
        hours={clinicHours}
        visit={visitTypes[0]}
        onBook={() => new Promise((r) => setTimeout(r, 700))}
      />
    </DemoFrame>
  );
}
```
