Placing from outside

Drag work that is not on the plan yet - from a backlog or a list of unassigned jobs - onto a lane, with the browser's own drag and drop. The drop reports where it should go; your application creates it.

import { useSchedule, subtaskFromPlace } from "@umriss-ui/schedule";

The browser hands over dragged data only on the drop, so declare what is being dragged in placing on your dragstart and clear it on dragend. The schedule then shows the ghost at the snapped time, with the findings the drop would create.

A place intent carries your item's key, the task, the lane and the times. The id of the new subtask is yours; subtaskFromPlace builds it from the intent.

Examples

Drag work in from the backlog

Set placing on your list's dragstart and clear it on dragend; the drop reports a place intent that subtaskFromPlace turns into work.

import { useState } from "react";
import { Card, Stack, Text } from "@umriss-ui/core";
import { applyIntent, subtaskFromPlace, useSchedule } from "@umriss-ui/schedule";
import type { Dependency, Intent, PlacingItem, 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;

const PEOPLE = [
  { id: "arjun", label: "Arjun Mehta" },
  { id: "chloe", label: "Chloe Durand" },
  { id: "eva", label: "Eva Novak" },
];

const PROJECTS: readonly Task[] = [
  { id: "shop", name: "Online shop relaunch", color: "light-dark(#2563eb, #6b9bff)" },
  { id: "portal", name: "Member portal", color: "light-dark(#7c3aed, #a98bfa)" },
];

const TODAY: readonly Subtask[] = [
  { id: "w-101", task: "portal", lane: "arjun", from: at(8), to: at(9, 30), name: "Sign-in with e-mail code" },
  { id: "w-103", task: "portal", lane: "chloe", from: at(8), to: at(8, 45), name: "Session handling" },
  { id: "w-104", task: "shop", lane: "chloe", from: at(9, 30), to: at(10, 45), name: "Basket across devices" },
  { id: "w-108", task: "portal", lane: "eva", from: at(11, 30), to: at(12, 15), name: "Test plan for sign-in" },
];

const HANDOVERS: readonly Dependency[] = [{ id: "h-101", from: "w-101", to: "w-108", lag: 20 * 60_000 }];

/* The backlog: what the drag carries, and a label for the list. */
const BACKLOG: readonly (PlacingItem & { label: string })[] = [
  { item: "w-121", label: "Fix sign-in redirect · 2 h", task: "portal", duration: 2 * HOUR },
  { item: "w-122", label: "Update privacy text · 1 h", task: "shop", duration: HOUR },
  { item: "w-123", label: "Accessibility audit · 4 h", task: "shop", duration: 4 * HOUR },
];

export default function DragFromTheBacklog() {
  const { Schedule, Lane, Subtasks, Dependencies } = useSchedule({ initialView: { domain: [at(5, 30), at(18)] } });
  const [plan, setPlan] = useState<{ work: readonly Subtask[]; placed: number; last: string }>({
    work: TODAY,
    placed: 0,
    last: "Drag an item onto a person",
  });
  const [placing, setPlacing] = useState<PlacingItem | null>(null);

  /* One functional update, so that a second intent in the same tick - and the
     counter the new id is built from - starts from the plan the first left. */
  const onIntent = (intent: Intent) =>
    setPlan((current) => {
      if (intent.kind !== "place") {
        return { ...current, work: current.work.map((item) => applyIntent(item, intent)), last: `${intent.kind} ${intent.subtask}` };
      }
      const placed = current.placed + 1;
      const id = `${intent.item}-${placed}`;
      return { work: [...current.work, subtaskFromPlace(intent, id)], placed, last: `place ${intent.item} on ${intent.lane} as ${id}` };
    });

  return (
    <Stack gap={3}>
      <Stack direction="row" gap={2} wrap>
        {BACKLOG.map((entry) => (
          <Card key={entry.item}>
            <div
              draggable
              data-waiting={entry.item}
              onDragStart={(event) => {
                event.dataTransfer.setData("text/plain", entry.item);
                event.dataTransfer.effectAllowed = "copy";
                setPlacing(entry);
              }}
              onDragEnd={() => setPlacing(null)}
              style={{ cursor: "grab", padding: "var(--u-space-2) var(--u-space-3)" }}
            >
              <Text size="sm">{entry.label}</Text>
            </div>
          </Card>
        ))}
      </Stack>
      <Schedule
        ariaLabel="Today's plan of three people, with the backlog beside it"
        height={196}
        intents={["place", "move", "lane"]}
        placing={placing}
        onIntent={onIntent}
      >
        {PEOPLE.map((person) => (
          <Lane key={person.id} id={person.id} label={person.label} />
        ))}
        <Dependencies data={HANDOVERS} />
        <Subtasks data={plan.work} tasks={PROJECTS} />
      </Schedule>
      <Text size="sm" mono tone="secondary" data-last-place>
        {plan.last}
      </Text>
    </Stack>
  );
}

When to use something else

Keyboard

The keys of First schedule apply here.

Props on this page

From ScheduleProps — the full table stands on First schedule.

NameTypeDefaultDescription
heightnumber400Height in CSS pixels, both bands included. The width is the host's.
intentsreadonly IntentKind[][]The intents the caller handles. Each one enables its interaction; none leaves a read-only schedule (ADR-0023).
placingPlacingItem | null—What the application is dragging in from outside while it drags it - its key, task, the length of its main time, its lead-in and lead-out. The browser hands the dragged data over only on the drop, so the ghost before it can only come from here: set it on your own dragstart, clear it on dragend. Without "place" in intents no drop is accepted.
childrenReactNode—Lane, Dependencies and Subtasks, in the order they are to stand and be drawn.

Events

NameTypeDefaultDescription
onIntent(intent: Intent) => void—Called once per intent when a drag ends. The data changes only if the caller changes it.

Accessibility

NameTypeDefaultDescription
ariaLabelrequiredstring—What the schedule shows, for a screen reader: "Plan of week 38".

From LaneProps — the full table stands on Lanes.

NameTypeDefaultDescription
idrequiredstring—The identity subtasks name in their lane.
labelReactNodeidWhat the lane header says - the person, vehicle or room. Real text, read by a screen reader.

From SubtasksProps — the full table stands on Subtasks.

NameTypeDefaultDescription
datarequiredreadonly Subtask[]—The subtasks, as the caller holds them. The schedule never changes one.
tasksrequiredreadonly Task[]—The tasks the subtasks belong to - their identity and their colour. A subtask whose task is not among them is drawn in a muted colour.

From Subtask — the full table stands on Subtasks.

NameTypeDefaultDescription
idrequiredstring—The caller's identity of the subtask.
taskrequiredstring—The id of the task it belongs to.
lanerequiredstring—The id of the lane it sits on.
fromrequirednumber—Start of the main time.
torequirednumber—End of the main time.
namestring—What the subtask is called, where the schedule names it.

From Task — the full table stands on Subtasks.

NameTypeDefaultDescription
idrequiredstring—The caller's identity of the task.
colorrequiredstring—Any CSS colour - a literal, a token, light-dark(…). The colour means what the caller's domain needs it to mean.
namestring—What the task is called, where the schedule names it.

From DependenciesProps — the full table stands on Dependencies.

NameTypeDefaultDescription
datarequiredreadonly Dependency[]—The dependencies, as the caller holds them.

From Dependency — the full table stands on Dependencies.

NameTypeDefaultDescription
idrequiredstring—The caller's identity of the dependency.
fromrequiredstring—The id of the subtask it leaves.
torequiredstring—The id of the subtask it arrives at.
lagrequirednumber—The least time between the two anchors; zero where the successor may begin as soon as the predecessor ends.

From ScheduleOptions — the full table stands on View.

NameTypeDefaultDescription
initialViewScheduleView—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.

From ScheduleView — the full table stands on View.

NameTypeDefaultDescription
domainreadonly [number, number]—The span in view, as two wall-clock instants. Absent, the schedule shows the extent of its subtasks, within zoomLimits.

Every page of @umriss-ui/schedule

Getting started

Plan

Time

Reading

Editing

Measuring

API index