# Time Grid

> A day of rooms or other resources: one row per room, time across, each booking a soft block in its event type's color. Book free time, and drag a booking to another time or room.

- Name: `time-grid`
- Status: beta
- Categories: data, display
- File: the install writes `time-grid.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `lucide-react`. The install adds them.
- Other registry items: `@arbokui/badge`, `@arbokui/hover-card`, `@arbokui/motion-timing`, `@arbokui/peek-card`, `@arbokui/tool-button`. The install adds them.
- Gallery: https://arbokui.com/components/time-grid
- Registry item: https://arbokui.com/r/time-grid.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/time-grid
   ```

## Use it for

- Room, desk, and equipment bookings: a day of rooms or other resources, in a Card.
- Pass resources, one per row, and bookings, each with its resource's id and 24-hour times.
- Set type for the event's color: training blue, match green, tournament teal, social violet, meeting slate.
- Pass now on today's day. A blue line marks it, the day opens on it, and bookings that have ended go quiet.
- Give a booking detail, PeekCard props, for its peek. Put the booking's own actions there, like Open, Move, and Cancel.
- Set closed on a room that cannot be booked, with the reason.
- Keep bookings in state. onMove hands back a booking's new room and times after each drop, and where it was when ⌘Z puts it back.
- Hand back a function from onCreate that takes the new booking away. ⌘Z calls it for 8 seconds.
- Tell the people booked in onSettle. It runs once the 8-second ⌘Z window ends, so ⌘Z never takes back a message already sent.
- Leave out onMove and onCreate for a day people only read.

## Avoid it for

- Telling the event type by color alone. Each booking's name and peek say the type in words.
- Weeks or months. This is one day.
- Your own undo button or timer. Moves and new bookings join the page's ⌘Z stack.
- Days much wider than the screen that people rearrange a lot. A drag does not scroll the day for you.

## Props

- `resources`: `{ id: string; name: string; meta?: string; closed?: string }[]`. The rooms or other resources, one row each. closed is why it is closed.
- `bookings`: `{ id: string; resource: string; start: string; end: string; title: string; type?: "training" | "match" | "tournament" | "social" | "meeting"; meta?: string; detail?: PeekCardProps | ReactNode }[]`. The day's bookings. resource is a resource's id, and times are 24-hour.
- `start`: `string`. Default `"09:00"`. The day's first time, 24-hour.
- `end`: `string`. Default `"18:00"`. The day's last time, 24-hour.
- `step`: `number`. Default `30`. Minutes the slot and a moved booking go by. Drags snap to it.
- `length`: `number`. Default `60`. Minutes a booking made with a click or Enter lasts. It stops short at the next booking.
- `now`: `string`. The time now, like "14:20", when the day shown is today.
- `hourWidth`: `number`. Default `72`. Pixels per hour before the day scrolls sideways.
- `label`: `string`. Default `"Bookings"`. Names the day for screen readers.
- `onCreate`: `(slot: { resource: string; start: string; end: string }) => void | (() => void)`. Free time was booked: by a drag, a click, a second tap, or Enter. Hand back a function that takes it away, and ⌘Z calls it for 8 seconds.
- `onMove`: `(booking: { id: string; resource: string; start: string; end: string }) => void`. A booking was dragged, or moved with the keys, to free time: its new room and times. ⌘Z moves it back, calling this with where it was.
- `onSettle`: `(booking: { id?: string; resource: string; start: string; end: string }, change: "move" | "create") => void`. A move, or a booking made here that onCreate can take away, is final: its ⌘Z window ended. A new booking has no id.
- `className`: `string`. Extra classes. Later classes win.

## Accessibility

- The day is a group named by label.
- Each booking reads its type, title, times, and room, like "Meeting, Design review, 11:00–12:30, Room 1". Ended bookings add "ended".
- The day is one Tab stop. ←→ move by step, ↑↓ go to the room above or below at the same time and pass closed ones, and Home and End go to the day's ends.
- On free time the stop is a button named like "Book Room 1, 10:30 to 11:30", and Enter books it. On a booking the stop is the booking. Enter goes into its peek, or says "11:00 is booked".
- A room's Clash is a Tab stop of its own, before the day. So is each booking on a closed room, since the keys pass closed rooms.
- Space picks a booking up. ←→ move it by step, Shift with ←→ changes its end, ↑↓ take it to the next open room, Space or Enter drops it, and Esc puts it back.
- A booking that is up, the slot while it shows, and a drag all own Esc, even inside a Sheet. The next Esc closes the sheet.
- Focus leaving a booking that is up drops it where it is, or puts it back from booked time.
- A polite live region says each step: picked up, each place it passes, where it dropped or went back, and why, like Taken or Closed.
- Every drag has the same moves on the keys. A mouse lifts a booking after 4 px, a finger after holding still for 350 ms, and a finger moving scrolls the day.
- ⌘Z (Ctrl+Z) puts a move back, or takes a new booking away, for 8 seconds. Screen readers hear what went back.
- The type is in words as well as color, and the peek's subtitle says it too.
- Booking text is the theme's foreground and muted text on a tint of 14% or less, so it keeps the card's contrast.
- The now line's time is text, read as "Now 14:20".
- Clash is a peek trigger. Its peek names the bookings that overlap.
- A closed room says Closed and why in words, not only with its shade.
- Forced colors keep a border on every booking and on the ended dot, and the rings on a lifted or landed booking become outlines.
- Reduced motion turns off the glide. Bookings step to their new places.

## Examples

### Book free time and drag bookings

```tsx
const [bookings, setBookings] = useState(DAY);

<Card className="gap-0 py-2">
  <TimeGrid
    label="Room bookings"
    resources={ROOMS}
    bookings={bookings}
    now="14:20"
    onCreate={(slot) => {
      const id = crypto.randomUUID();
      setBookings((all) => [...all, { id, ...slot, title: "New booking" }]);
      return () => setBookings((all) => all.filter((booking) => booking.id !== id));
    }}
    onMove={(moved) => setBookings((all) => all.map((booking) => (booking.id === moved.id ? { ...booking, ...moved } : booking)))}
    onSettle={tellPeopleBooked}
  />
</Card>
```

### Read only, with nothing to book or move

```tsx
<Card className="gap-0 py-2">
  <TimeGrid label="Evening bookings" start="17:00" end="21:00" hourWidth={120} resources={ROOMS} bookings={EVENING} />
</Card>
```
