02examples
Room booking
Four meeting rooms as timeline lanes — drag to book, drag across rooms to reassign, drag an edge to resize.
Four rooms, one working day, 7 AM to 7 PM. Each booking is a bar on its room's row; drag on empty canvas to book a new one, drag an existing bar onto a different room to move the meeting there, drag either edge to change how long it runs. The boardroom's row is taller than the others — it books solid often enough that it earns a fourth overlap slot before bookings collapse into "+N more".
liveRoom bookingviewlaneslaneAdapterbyResourceId
loading
"use client";
import { useState } from "react";
import { Kloq, type CalEvent } from "kloq";
import { ROOMS, ROOM_ADAPTER, roomBookings } from "./data";
export default function RoomBookingExample() {
const [events, setEvents] = useState<CalEvent[]>(() => roomBookings());
return (
<div className="h-[36rem]">
<Kloq
view="timeline"
lanes={ROOMS}
laneAdapter={ROOM_ADAPTER}
timelineWindow={{ startOffsetMinutes: 7 * 60, spanMinutes: 12 * 60 }}
events={events}
onEventsChange={setEvents}
persistence={false}
designMode={false}
storage={false}
>
<Kloq.Toolbar>
<Kloq.Nav />
<span className="font-mono text-xs text-muted-foreground">4 rooms · 7 AM–7 PM</span>
</Kloq.Toolbar>
</Kloq>
</div>
);
}
import { byResourceId, formatInstant, type CalEvent, type Lane } from "kloq";
export type Booking = CalEvent & { resourceId: string };
/** Each room is a lane; a booking names its room through a plain `resourceId` field. */
export const ROOMS: Lane[] = [
{ id: "sunroom", label: "Sunroom", sublabel: "2nd floor · 8 seats" },
{ id: "boardroom", label: "Boardroom", sublabel: "3rd floor · 14 seats", height: 132 },
{ id: "studio", label: "Studio", sublabel: "Ground floor · 6 seats" },
{ id: "focus-pod", label: "Focus pod", sublabel: "3rd floor · 2 seats" },
];
/** `byResourceId()` reads `resourceId` — the default a host gets by omitting `laneAdapter` entirely. */
export const ROOM_ADAPTER = byResourceId();
/** Today at the given local hour/minute, as an offset-qualified instant. */
function at(hour: number, minute = 0): string {
const d = new Date();
d.setHours(hour, minute, 0, 0);
return formatInstant(d);
}
export function roomBookings(): Booking[] {
return [
{ id: "rb-standup", title: "Standup", resourceId: "studio", start: at(9, 0), end: at(9, 15), color: "blue" },
{ id: "rb-budget", title: "Budget review", resourceId: "boardroom", start: at(9, 0), end: at(11, 0), color: "violet" },
{ id: "rb-interview", title: "Interview: Sam Okafor", resourceId: "boardroom", start: at(10, 0), end: at(10, 45), color: "amber" },
{ id: "rb-focus", title: "Focus block", resourceId: "focus-pod", start: at(8, 0), end: at(12, 0), color: "emerald" },
{ id: "rb-demo", title: "Product demo", resourceId: "studio", start: at(13, 0), end: at(14, 0), color: "rose" },
{ id: "rb-1on1", title: "1:1: Priya", resourceId: "sunroom", start: at(14, 0), end: at(14, 30), color: "blue" },
{ id: "rb-allhands", title: "All-hands prep", resourceId: "boardroom", start: at(15, 0), end: at(16, 0), color: "violet" },
];
}
How it works
view="timeline"withlanes={ROOMS}— fourLaneobjects, one per room.laneAdapter={byResourceId()}reads each booking's plainresourceIdfield to place it; that's also the default, so this example passes it explicitly only to name it.timelineWindow={{ startOffsetMinutes: 7 * 60, spanMinutes: 12 * 60 }}is derived mode — nostart/end— so the toolbar's ‹ › and "Today" move the window the same way they'd move any other view's range (Resource timeline).- Dragging a bar from the Studio row onto the Boardroom row doesn't just move
it sideways — the drop calls
byResourceId()'sassign(event, "boardroom"), which returns{ resourceId: "boardroom" }, committed like any other change (Resources & lanes). - The Boardroom's
height: 132raises its overlap cap from 3 to 4 — the cap is height ÷ bar height, floored, so the exact numbers move withdensity(this example's comfortable default: 30px bars, 96px rows). It's the one lever for "this room double-books a lot," rather than a separate max-overlaps prop (the overflow cap). events/onEventsChangekeep the bookings in React state (Controlled state); noconstraintshere, but every one ofminDate/maxDate/isSlotDisabled/canDropwould apply to these drags exactly as they do on the calendar's own grid (Constraints).
Take it further
- Add
constraintsto refuse double-booking a room outright:isSlotDisabledreading the room's own existing bookings, the same overlap check the booking page runs against a host's busy time. - Swap
byResourceId()for a hand-rolledLaneAdapterthat groups by floor instead of by room — the custom adapter worked example does exactly this for triage status. - Pin
timelineWindow'sstart/endto a specific business day instead of deriving off the anchor, the way Shift scheduler pins the current week — trading next/prev navigation for an exact, server-driven window.