# Interactions

> Package `@umriss-ui/schedule`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/schedule/interactions/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/schedule/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/schedule/llms-full.txt).

Reports what the pointer does on the plan - hover, click, right-click - with what it hit and the time and lane under it. Reach for it to open a menu, fill a detail panel or write a status line.

```ts
import { useSchedule } from "@umriss-ui/schedule";
```

Demo page: https://romanhaendler.github.io/umriss-ui/schedule/interactions/

A hit names a subtask together with the part of it (lead-in, main time, lead-out), a dependency, a lane, or nothing.

While `onInteraction` is set, a right-click opens no browser menu: your application is expected to answer it.

## Examples

### Report what the pointer is on

`onInteraction` hears hover, click and right-click with the target, its part, the time and the lane - here written into a status line.

```tsx
import { useState } from "react";
import { Stack, Text } from "@umriss-ui/core";
import { useSchedule } from "@umriss-ui/schedule";
import type { Dependency, ScheduleInteraction, Subtask, Task } from "@umriss-ui/schedule";

const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const min = (n: number) => n * 60_000;

const CONSIGNMENTS: readonly Task[] = [
  { id: "c-2041", name: "C-2041 Holloway Garden Supplies", color: "light-dark(#2563eb, #6b9bff)" },
];

const LEGS: readonly Subtask[] = [
  { id: "c-2041-1", task: "c-2041", lane: "truck-118", from: at(6), to: at(7), leadIn: min(15), leadOut: min(10) },
  { id: "c-2041-2", task: "c-2041", lane: "van-402", from: at(8), to: at(10, 30), leadIn: min(30), leadOut: min(15) },
];

const TRANSFERS: readonly Dependency[] = [{ id: "t-2041-1", from: "c-2041-1", to: "c-2041-2", lag: min(10) }];

const describe = (interaction: ScheduleInteraction): string => {
  const { hit } = interaction;
  /* To the nearest minute: a pixel is about a minute wide here. */
  const time = new Date(Math.round(interaction.time / 60_000) * 60_000).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
  const target =
    hit.kind === "subtask"
      ? `subtask ${hit.subtask.id} (${hit.part})`
      : hit.kind === "dependency"
        ? `dependency ${hit.dependency.id}`
        : hit.kind === "lane"
          ? "free time"
          : "nothing";
  return `${interaction.type}: ${target} at ${time}, lane ${interaction.lane ?? "none"}`;
};

export default function ReportThePointer() {
  const { Schedule, Lane, Subtasks, Dependencies } = useSchedule({ initialView: { domain: [at(5, 30), at(18)] } });
  const [last, setLast] = useState("Move the pointer over the plan");

  return (
    <Stack gap={3}>
      <Schedule
        ariaLabel="One consignment on a truck and a van"
        height={152}
        onInteraction={(interaction) => setLast(describe(interaction))}
      >
        <Lane id="truck-118" label="Truck FP 118 R" />
        <Lane id="van-402" label="Van FP 402 R" />
        <Dependencies data={TRANSFERS} />
        <Subtasks data={LEGS} tasks={CONSIGNMENTS} />
      </Schedule>
      <Text size="sm" mono tone="secondary" data-last-interaction>
        {last}
      </Text>
    </Stack>
  );
}
```

### Open a context menu

On a `contextmenu` interaction, open `ContextMenu` from @umriss-ui/core at `clientX` and `clientY`; its entries change your data.

```tsx
import { useState } from "react";
import { ContextMenu, MenuItem } from "@umriss-ui/core";
import { applyIntent, useSchedule } from "@umriss-ui/schedule";
import type { ScheduleInteraction, Subtask, Task } from "@umriss-ui/schedule";

/* Data from the operations world, written out here so the example runs on its own. */

const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();

interface Engineer {
  id: string;
  name: string;
  team: string;
}

const ENGINEERS: readonly Engineer[] = [
  { id: "priya", name: "Priya Raman", team: "Payments" },
  { id: "jonas", name: "Jonas Keller", team: "Payments" },
  { id: "ada", name: "Ada Mwangi", team: "Identity" },
  { id: "tomasz", name: "Tomasz Nowak", team: "Discovery" },
  { id: "leila", name: "Leila Haddad", team: "Discovery" },
  { id: "sam", name: "Sam Okafor", team: "Messaging" },
  { id: "ines", name: "Ines Duarte", team: "Integrations" },
  { id: "felix", name: "Felix Brandt", team: "Insights" },
];

interface OnCall {
  id: string;
  engineer: string;
  rotation: "primary" | "secondary";
  from: number;
  to: number;
}

/** Monday 16 to Monday 23 March, handed over every morning at 09:00: the
    secondary of one day is the primary of the next. */
const ONCALL: readonly OnCall[] = Array.from({ length: 7 }, (_, day) => [
  { rotation: "primary" as const, engineer: ENGINEERS[day % ENGINEERS.length]!.id },
  { rotation: "secondary" as const, engineer: ENGINEERS[(day + 1) % ENGINEERS.length]!.id },
].map(({ rotation, engineer }) => ({
  id: `${rotation}-${16 + day}`,
  engineer,
  rotation,
  from: at(16 + day, 9),
  to: at(17 + day, 9),
}))).flat();

const day = (d: number, hours = 0) => new Date(2026, 2, d, hours).getTime();

const ROTATIONS: readonly Task[] = [
  { id: "primary", name: "Primary", color: "light-dark(#2563eb, #6b9bff)" },
  { id: "secondary", name: "Secondary", color: "light-dark(#0d9488, #3cc7b8)" },
];

/* The rotation is the task, the engineer the lane. */
const START: readonly Subtask[] = ONCALL.map((duty) => ({
  id: duty.id,
  task: duty.rotation,
  lane: duty.engineer,
  from: duty.from,
  to: duty.to,
  name: duty.rotation === "primary" ? "Primary" : "Secondary",
}));

export default function OpenAContextMenu() {
  const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [day(16, 6), day(23, 12)] } });
  const [rota, setRota] = useState<readonly Subtask[]>(START);
  const [menu, setMenu] = useState<{ interaction: ScheduleInteraction; duty: Subtask } | null>(null);

  /* While `onInteraction` is set, a right-click opens no browser menu. */
  const onInteraction = (interaction: ScheduleInteraction) => {
    if (interaction.type !== "contextmenu") return;
    setMenu(interaction.hit.kind === "subtask" ? { interaction, duty: interaction.hit.subtask } : null);
  };

  return (
    <>
      <Schedule
        ariaLabel="On-call rota, 16 to 23 March"
        height={420}
        onInteraction={onInteraction}
      >
        {ENGINEERS.map((engineer) => (
          <Lane key={engineer.id} id={engineer.id} label={engineer.name} />
        ))}
        <Subtasks data={rota} tasks={ROTATIONS} />
      </Schedule>
      <ContextMenu
        open={menu !== null}
        position={{ x: menu?.interaction.clientX ?? 0, y: menu?.interaction.clientY ?? 0 }}
        onOpenChange={(open) => {
          if (!open) setMenu(null);
        }}
        ariaLabel={menu !== null ? `Hand over ${menu.duty.id}` : "Hand over"}
      >
        {menu !== null &&
          ENGINEERS.filter((engineer) => engineer.id !== menu.duty.lane).map((engineer) => (
            <MenuItem
              key={engineer.id}
              onSelect={() => {
                const intent = { kind: "lane", subtask: menu.duty.id, lane: engineer.id } as const;
                setRota((current) => current.map((duty) => applyIntent(duty, intent)));
              }}
            >
              Hand to {engineer.name}
            </MenuItem>
          ))}
      </ContextMenu>
    </>
  );
}
```

## When to use something else

- You only need to know which task was picked → Selection
- You need the time at a point outside an interaction → The handle

## Keyboard

The keys of [First schedule](https://romanhaendler.github.io/umriss-ui/schedule/schedule/#keyboard-schedule) apply here.

## API

### `ScheduleInteraction`

| Prop | Type | Default | Description |
|---|---|---|---|
| `type` *required* | `"click" \| "contextmenu" \| "hover"` | — | `"hover"` is reported when the target under the pointer changes, not on every movement.<br>Shown in: [Report what the pointer is on](https://romanhaendler.github.io/umriss-ui/schedule/interactions/#report-the-pointer), [Open a context menu](https://romanhaendler.github.io/umriss-ui/schedule/interactions/#open-a-context-menu), [Show a detail panel instead (Tooltip)](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#detail-panel-instead) and 1 more |
| `hit` *required* | [`ScheduleHit`](https://romanhaendler.github.io/umriss-ui/schedule/interactions/#type-ScheduleHit) | — | What the pointer is on.<br>Shown in: [Report what the pointer is on](https://romanhaendler.github.io/umriss-ui/schedule/interactions/#report-the-pointer), [Open a context menu](https://romanhaendler.github.io/umriss-ui/schedule/interactions/#open-a-context-menu), [Show a detail panel instead (Tooltip)](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#detail-panel-instead) and 2 more |
| `clientX` *required* | `number` | — | The pointer's horizontal client coordinate - where a `ContextMenu` opens.<br>Shown in: [Open a context menu](https://romanhaendler.github.io/umriss-ui/schedule/interactions/#open-a-context-menu), [Place a mark of your own (The handle)](https://romanhaendler.github.io/umriss-ui/schedule/handle/#place-a-mark), [Replan the day on the line (Scenarios)](https://romanhaendler.github.io/umriss-ui/schedule/#replan-the-day-on-the-line) |
| `clientY` *required* | `number` | — | The pointer's vertical client coordinate.<br>Shown in: [Open a context menu](https://romanhaendler.github.io/umriss-ui/schedule/interactions/#open-a-context-menu), [Place a mark of your own (The handle)](https://romanhaendler.github.io/umriss-ui/schedule/handle/#place-a-mark), [Replan the day on the line (Scenarios)](https://romanhaendler.github.io/umriss-ui/schedule/#replan-the-day-on-the-line) |
| `time` *required* | `number` | — | The wall-clock instant under the pointer.<br>Shown in: [Report what the pointer is on](https://romanhaendler.github.io/umriss-ui/schedule/interactions/#report-the-pointer) |
| `lane` *required* | `string \| null` | — | The lane under the pointer, or null below the last one.<br>Shown in: [Report what the pointer is on](https://romanhaendler.github.io/umriss-ui/schedule/interactions/#report-the-pointer) |

### Types on this page

#### `ScheduleHit`

What a pointer is on.

```ts
type ScheduleHit =
  | { readonly kind: "subtask"; readonly subtask: Subtask; readonly part: "leadIn" | "main" | "leadOut" }
  | { readonly kind: "dependency"; readonly dependency: Dependency }
  | { readonly kind: "milestone"; readonly milestone: Milestone }
  | { readonly kind: "lane"; readonly lane: string }
  | { readonly kind: "nothing" };
```
