Makes a drag land on a raster - the axis' own step, a fixed duration, or a pattern with an offset - so dropped work starts on a time a planner can read.
import { useSchedule, snapTime } from "@umriss-ui/schedule";
Without snap a drag lands on the fine band's step, which follows the zoom. A number is a step in milliseconds; { step, offset } counts from local midnight; false places to the minute.
Snapping shapes the ghost and so the intent; it never moves data that is not being dragged. snapTime is the same arithmetic for your own use.
Without snap, a drag lands on the fine band's step: quarter hours when zoomed in, whole hours when zoomed out.
import { useState } from "react";
import { applyIntent, useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const TOURS: readonly Task[] = [{ id: "river", name: "Riverside depot tours", color: "light-dark(#2563eb, #6b9bff)" }];
const START: readonly Subtask[] = [{ id: "T-04", task: "river", lane: "van-402", from: at(7, 30), to: at(12, 10) }];
export default function SnapToTheTicks() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6), at(16)] } });
const [tours, setTours] = useState(START);
return (
<Schedule
ariaLabel="A tour on the default raster"
height={100}
intents={["move"]}
onIntent={(intent) => setTours((current) => current.map((tour) => applyIntent(tour, intent)))}
>
<Lane id="van-402" label="Van FP 402 R" />
<Subtasks data={tours} tasks={TOURS} />
</Schedule>
);
}
Give snap a number of milliseconds - here half a day - and every drag lands on that raster, whatever the zoom.
import { useState } from "react";
import { applyIntent, useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";
const day = (d: number, hours = 0) => new Date(2026, 2, d, hours).getTime();
const HALF_DAY = 12 * 60 * 60_000;
const PROJECTS: readonly Task[] = [{ id: "shop", name: "Online shop relaunch", color: "light-dark(#2563eb, #6b9bff)" }];
const START: readonly Subtask[] = [
{ id: "w-104", task: "shop", lane: "chloe", from: day(17), to: day(18, 12), name: "Basket keeps items across devices" },
{ id: "w-107", task: "shop", lane: "noah", from: day(18, 12), to: day(20), name: "Search results layout" },
];
export default function SnapToADuration() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [day(16), day(21)] } });
const [work, setWork] = useState(START);
return (
<Schedule
ariaLabel="Work planned in half days"
height={140}
snap={HALF_DAY}
intents={["move", "stretch"]}
onIntent={(intent) => setWork((current) => current.map((item) => applyIntent(item, intent)))}
>
<Lane id="chloe" label="Chloe Durand" />
<Lane id="noah" label="Noah Fischer" />
<Subtasks data={work} tasks={PROJECTS} />
</Schedule>
);
}
A snap raster with a step and an offset: eight-hour watches handed over at 06:00, 14:00 and 22:00 local time.
import { useState } from "react";
import { applyIntent, useSchedule } from "@umriss-ui/schedule";
import type { SnapRaster, Subtask, Task } from "@umriss-ui/schedule";
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const HOUR = 3_600_000;
/* Offset from local midnight, whatever the time zone. */
const HANDOVERS: SnapRaster = { step: 8 * HOUR, offset: 6 * HOUR };
const ENGINEERS: readonly Task[] = [{ id: "ada", name: "Ada Mwangi", color: "light-dark(#7c3aed, #a98bfa)" }];
const START: readonly Subtask[] = [{ id: "watch-1", task: "ada", lane: "primary", from: at(6), to: at(14) }];
export default function SnapToHandovers() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(4), at(24)] } });
const [watches, setWatches] = useState(START);
return (
<Schedule
ariaLabel="An on-call watch on the handover raster"
height={110}
snap={HANDOVERS}
intents={["move"]}
onIntent={(intent) => setWatches((current) => current.map((watch) => applyIntent(watch, intent)))}
>
<Lane id="primary" label="Primary" />
<Subtasks data={watches} tasks={ENGINEERS} />
</Schedule>
);
}
snap={false} lets a drag land on any minute - for times that follow the pointer rather than a raster.
import { useState } from "react";
import { applyIntent, useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const ENGINEERS: readonly Task[] = [{ id: "sam", name: "Sam Okafor", color: "light-dark(#0d9488, #3cc7b8)" }];
const START: readonly Subtask[] = [{ id: "window-1", task: "sam", lane: "notifications", from: at(13, 5), to: at(13, 50), name: "Maintenance window" }];
export default function NoSnapping() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(10), at(18)] } });
const [windows, setWindows] = useState(START);
return (
<Schedule
ariaLabel="A maintenance window placed freely"
height={100}
snap={false}
intents={["move"]}
onIntent={(intent) => setWindows((current) => current.map((one) => applyIntent(one, intent)))}
>
<Lane id="notifications" label="Notifications" />
<Subtasks data={windows} tasks={ENGINEERS} />
</Schedule>
);
}
The keys of First schedule apply here.
From ScheduleProps — the full table stands on First schedule.
| Name | Type | Default | Description |
|---|---|---|---|
height | number | 400 | Height in CSS pixels, both bands included. The width is the host's. |
snap | | "ticks" | "ticks" | The raster a drag lands on: "ticks" - the fine band's current step -, a
step in milliseconds from local midnight, a step with an offset (shifts at
06:00, 14:00 and 22:00 are eight hours offset by six), or false for none.
It shapes the ghost and the intent, never the data. |
intents | readonly IntentKind[] | [] | The intents the caller handles. Each one enables its interaction; none leaves a read-only schedule (ADR-0023). |
children | ReactNode | — | Lane, Dependencies and Subtasks, in the order they are to stand and
be drawn. |
| Name | Type | Default | Description |
|---|---|---|---|
onIntent | (intent: Intent) => void | — | Called once per intent when a drag ends. The data changes only if the caller changes it. |
| Name | Type | Default | Description |
|---|---|---|---|
ariaLabelrequired | string | — | What the schedule shows, for a screen reader: "Plan of week 38". |
From LaneProps — the full table stands on Lanes.
From SubtasksProps — the full table stands on Subtasks.
From Subtask — the full table stands on Subtasks.
| Name | Type | Default | Description |
|---|---|---|---|
idrequired | string | — | The caller's identity of the subtask. |
taskrequired | string | — | The id of the task it belongs to. |
lanerequired | string | — | The id of the lane it sits on. |
fromrequired | number | — | Start of the main time. |
torequired | number | — | End of the main time. |
name | string | — | What the subtask is called, where the schedule names it. |
From Task — the full table stands on Subtasks.
From ScheduleOptions — the full table stands on View.
| Name | Type | Default | Description |
|---|---|---|---|
initialView | ScheduleView | — | The view to start from, read once when the hook first runs; a different
value on a later render is ignored, as React ignores a changed
defaultValue. To go to a view later, call setView. The schedule
remembers none: where a view is kept is the application's decision. |