> ## Documentation Index
> Fetch the complete documentation index at: https://super-calendar.afonsojramos.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Resource timeline

> A one-day timeline with a lane per resource (room, person, machine), horizontal or vertical.

<Note>
  Available on **both renderers**, including drag-to-move and edge-resize via `onDragEvent`. The web
  renderer (`@super-calendar/dom`) adds per-slot class styling; the React Native renderer
  (`@super-calendar/native`) styles via `theme.containers`. Import from the package you're using.
</Note>

`ResourceTimeline` lays a single day out horizontally: each **resource** is a row,
the x-axis is the day's hours, and every event sits in its resource's row at its
start time. It's the classic scheduling board — rooms, people, or machines down the
side, time across the top — and the MIT answer to the "resource timeline" views
other libraries gate behind a paid tier.

```tsx theme={null}
import { type CalendarEvent, ResourceTimeline } from "@super-calendar/dom";

const resources = [
  { id: "room-a", title: "Room A" },
  { id: "room-b", title: "Room B" },
];

const events: CalendarEvent<{ resourceId: string }>[] = [
  {
    resourceId: "room-a",
    title: "Standup",
    start: new Date(2026, 5, 26, 9),
    end: new Date(2026, 5, 26, 10),
  },
  {
    resourceId: "room-b",
    title: "Review",
    start: new Date(2026, 5, 26, 11),
    end: new Date(2026, 5, 26, 12),
  },
];

<ResourceTimeline date={new Date(2026, 5, 26)} resources={resources} events={events} />;
```

## How events map to rows

Each event is placed in the row whose `id` matches its resource. By default that's
read from `event.resourceId`; pass a `resourceId` accessor for a different shape:

```tsx theme={null}
<ResourceTimeline
  date={date}
  resources={resources}
  events={events}
  resourceId={(event) => event.room}
/>
```

Overlapping events **in the same row** stack into sub-lanes (via the shared
`layoutDayEvents`), so nothing is hidden behind anything else.

## Vertical orientation

On a narrow screen the horizontal axis has to scroll sideways, which reads poorly.
Pass `orientation="vertical"` to flip the layout into the time grid's shape: the
hour axis runs down the left, each resource becomes a **column**, and the columns
flex to share the available width, so a phone shows the whole board at once.

```tsx theme={null}
<ResourceTimeline
  date={date}
  orientation="vertical"
  resources={resources}
  events={events}
  startHour={7}
  endHour={20}
/>
```

A common pattern is switching on the viewport, like the example app does:

```tsx theme={null}
orientation={width < 600 ? "vertical" : "horizontal"}
```

Everything else carries over: overlapping events share their column as side-by-side
sub-lanes, drag-to-move follows the vertical axis, and resize moves to the bottom
edge. On React Native the resource headers stay fixed while the hours scroll, so
give the timeline a bounded height (a flexed or fixed-height parent); on the web
the track scrolls inside the component when you constrain its height. `hourHeight` (default 48) sets the pixels per hour; `hourWidth`, `rowHeight`,
and `labelWidth` apply to the horizontal orientation only. Custom renderers get
`height` (the bar's pixel height) in their `ResourceEventArgs`; `width` is 0 in
this orientation because the columns flex.

## Paging resources

Columns flex to share the width, so past a handful of resources they get too
narrow, especially at phone widths. `resourcesPerPage` caps how many lanes show
at once; `resourcePage` (0-based, clamped to the last page) picks which ones.
It's controlled, like `date`: wire your own prev/next buttons to a state value.

```tsx theme={null}
const [page, setPage] = useState(0);
const pages = Math.ceil(resources.length / 3);

<ResourceTimeline
  date={date}
  orientation="vertical"
  resources={resources}
  events={events}
  resourcesPerPage={3}
  resourcePage={page}
/>;
// Your controls:
<Button title="Next rooms" onPress={() => setPage((p) => Math.min(p + 1, pages - 1))} />;
```

Both props work in either orientation (they window rows in the horizontal
layout too) and on both renderers.

## Sizing & window

* `startHour` / `endHour` — the visible hour window (default 0–24).
* `hourWidth` — pixels per hour along the horizontal axis (default 80); the track
  scrolls horizontally when it overflows. Horizontal only.
* `hourHeight` — pixels per hour down the vertical axis (default 48). Vertical only.
* `rowHeight` — height of each resource row (default 56). Horizontal only.
* `labelWidth` — width of the left resource-label column (default 140). Horizontal only.
* `ampm` — 12-hour axis labels.

## Moving & resizing

Pass `onDragEvent` to make the board interactive: drag a bar to move it along the
time axis **or across lanes**, or drag its trailing edge to resize. It's called
with the proposed new start/end and the lane the bar was dropped in (the original
lane when it didn't cross); retarget the event's resource from that fourth
argument. Return `false` to reject the drop (the bar snaps back). Movement snaps
to `dragStepMinutes` (default 15). Only the currently visible lanes are drop
targets, so cap a long list with `resourcesPerPage` and page through it.

To keep the board time-only, reject any lane change: compare the dropped
`resource.id` to the event's current one and `return false` when they differ.

On the web, drag with a mouse or pen; a bar being dragged carries `data-dragging`
for styling. On React Native, long-press a bar to pick it up, then drag along the
time axis or sideways into another lane; the same move and cross-lane moves are
also exposed as screen-reader actions on the bar. In the vertical orientation the
time axis runs down and resize moves to the bottom edge; lanes are the columns, so
a cross-lane drag goes sideways.

```tsx theme={null}
import { ResourceTimeline } from "@super-calendar/dom";

<ResourceTimeline
  date={date}
  resources={resources}
  events={events}
  onDragEvent={(event, start, end, resource) => {
    if (event.locked) return false;
    setEvents((prev) =>
      prev.map((e) => (e === event ? { ...e, start, end, resourceId: resource.id } : e)),
    );
  }}
/>;
```

## Creating and pressing empty space

The board takes the same empty-space interactions as the time grid, with the
lane's resource added to each callback so you know **where** as well as when:

* `onPressCell(at, resource)` — tap an empty slot; `at` is snapped to
  `dragStepMinutes`.
* `onCreateEvent(start, end, resource)` — sweep out a new event on empty lane
  space: drag with a mouse or pen on the web, long-press then drag on React
  Native. A ghost previews the range while you drag.
* `onLongPressEvent(event)` / `onLongPressCell(at, resource)` — React Native
  long-presses. A bar's long-press is claimed by `onDragEvent` when set (so
  `onLongPressEvent` fires only for non-draggable bars), and empty-space
  long-press is claimed by `onCreateEvent` when set (so `onLongPressCell` fires
  only without it).

```tsx theme={null}
<ResourceTimeline
  date={date}
  resources={resources}
  events={events}
  onPressCell={(at, resource) => openBookingForm(resource, at)}
  onCreateEvent={(start, end, resource) =>
    setEvents((prev) => [...prev, { title: "New booking", start, end, resourceId: resource.id }])
  }
/>
```

## Business hours

`businessHours` shades the closed hours of each lane. It takes the same shape
as the Calendar's prop plus the lane's resource, so lanes can keep different
opening hours; return `null` for a fully closed lane. A date-only function
(the exact one you pass to `Calendar`) is accepted as-is.

```tsx theme={null}
<ResourceTimeline
  date={date}
  resources={resources}
  events={events}
  businessHours={(day, resource) => (resource.id === "vault" ? null : { start: 9, end: 17 })}
/>
```

To render the closed region yourself (a label, an icon, a pattern), add
`renderBusinessHours`. The board keeps positioning each band and calls you with
`{ date, start, end, resource }`; the themed tint is dropped so your output owns
the look. The band is decorative: it stays non-interactive and hidden from
assistive tech, so return visuals, not buttons or announced text:

```tsx theme={null}
<ResourceTimeline
  date={date}
  resources={resources}
  events={events}
  businessHours={() => ({ start: 9, end: 17 })}
  renderBusinessHours={({ resource }) => <ClosedBand label={`${resource.title} closed`} />}
/>
```

## React Native

The native renderer exposes the same board and props (`date`, `resources`, `events`,
`orientation`, `resourceId`, `resourcesPerPage`, `resourcePage`, `startHour`/`endHour`, `hourWidth`, `hourHeight`, `rowHeight`, `labelWidth`, `ampm`,
`renderEvent`, `onPressEvent`, `onPressCell`, `onCreateEvent`, `businessHours`,
`renderBusinessHours`), plus
`onDragEvent` / `dragStepMinutes` for **long-press drag-to-move, cross-lane drag, and edge-resize**
and the long-press variants `onLongPressEvent` / `onLongPressCell`. It reads the theme from context, so
restyle via `theme.containers` (see [Theming](/guides/theming)):

```tsx theme={null}
import { ResourceTimeline } from "@super-calendar/native";

<ResourceTimeline
  date={date}
  resources={resources}
  events={events}
  onPressEvent={setSelected}
  onDragEvent={(event, start, end, resource) => {
    setEvents((prev) =>
      prev.map((e) => (e === event ? { ...e, start, end, resourceId: resource.id } : e)),
    );
  }}
/>;
```

Because dragging is gesture-only, native draggable bars also carry screen-reader
actions: **move earlier / later**, **extend / shorten**, and **move to** the
adjacent lanes, each stepping by `dragStepMinutes` (the lane moves retarget the
resource). VoiceOver / TalkBack users invoke them from the bar's actions menu to
reschedule or reassign without a gesture, through the same `onDragEvent`. (The
visual resize grip is a pointer/touch affordance and isn't a separate focus
target.)

## Styling

Like the other views, every part is a [slot](/guides/styling): `header`, `corner`,
`timeAxis`, `hourTick`, `resourceLabel`, `row`, `track`, `gridLines`,
`businessHours`, `backgroundEvent`, `event`, `eventBox`, `nowIndicator`, and `createGhost`. Pass `classNames` /
`styles`, or a `renderEvent` for a fully custom bar. It also takes the shared
[theme](/guides/theming).

See the **resource** tab on the [live demo](/demo).
