# Autopilot: Agent at Work

> An AI agent books a meeting in a calendar built from plain markup. It opens a new event, names it, finds a free slot, picks a guest from the autocomplete and sends the invite, narrating each step in its label. Move your mouse in or tab into it and it steps aside to the corner set with `park`; leave, and two seconds later it replays the chapter it was in.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/autopilot-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/autopilot-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/autopilot-demo-light-4a1c0cdbc364e380.png
- Registry item: https://keyframeui.com/r/autopilot-demo.json
- Categories: cursor, interaction, showcase
- Listed: 2026-10-14, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/autopilot-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/autopilot.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/autopilot#autopilot-demo

## Files

### `registry/demos/autopilot-demo.tsx`

```tsx
"use client";

import * as React from "react";

import { Autopilot, type AutopilotChapter } from "@/components/ui/autopilot";

const days = [
  { key: "mon", label: "Mon", date: 13, wide: true },
  { key: "tue", label: "Tue", date: 14 },
  { key: "wed", label: "Wed", date: 15 },
  { key: "thu", label: "Thu", date: 16 },
  { key: "fri", label: "Fri", date: 17, wide: true },
];

const hours = [9, 10, 11, 12, 13, 14, 15, 16];
const ROW = 40; // px per hour

const tones = {
  sky: "border-sky-500 bg-sky-50 text-sky-900 dark:bg-sky-400/15 dark:text-sky-100",
  teal: "border-teal-500 bg-teal-50 text-teal-900 dark:bg-teal-400/15 dark:text-teal-100",
  amber: "border-amber-500 bg-amber-50 text-amber-900 dark:bg-amber-400/15 dark:text-amber-100",
  rose: "border-rose-500 bg-rose-50 text-rose-900 dark:bg-rose-400/15 dark:text-rose-100",
  slate: "border-zinc-400 bg-zinc-100 text-zinc-700 dark:bg-white/10 dark:text-zinc-200",
};

const events: { id: string; day: string; start: number; end: number; title: string; tone: keyof typeof tones }[] = [
  { id: "mon-standup", day: "mon", start: 9, end: 9.5, title: "Standup", tone: "sky" },
  { id: "mon-1on1", day: "mon", start: 10.5, end: 11.5, title: "1:1 with Sam", tone: "teal" },
  { id: "mon-focus", day: "mon", start: 14, end: 16, title: "Focus time", tone: "slate" },
  { id: "tue-standup", day: "tue", start: 9, end: 9.5, title: "Standup", tone: "sky" },
  { id: "tue-roadmap", day: "tue", start: 11, end: 12.5, title: "Roadmap sync", tone: "rose" },
  { id: "calls", day: "tue", start: 13, end: 15, title: "Customer calls", tone: "amber" },
  { id: "wed-standup", day: "wed", start: 9, end: 9.5, title: "Standup", tone: "sky" },
  { id: "wed-lunch", day: "wed", start: 12, end: 13, title: "Lunch & learn", tone: "teal" },
  { id: "wed-review", day: "wed", start: 15, end: 17, title: "Sprint review", tone: "rose" },
  { id: "thu-panel", day: "thu", start: 10, end: 11.5, title: "Hiring panel", tone: "amber" },
  { id: "thu-crit", day: "thu", start: 13, end: 14, title: "Design crit", tone: "slate" },
  { id: "fri-standup", day: "fri", start: 9, end: 9.5, title: "Standup", tone: "sky" },
  { id: "fri-retro", day: "fri", start: 11, end: 12, title: "Retro", tone: "teal" },
];

const people = [
  { id: "priya", name: "Priya Raman", email: "priya@northwind.co", tint: "bg-rose-500" },
  { id: "pablo", name: "Pablo Ortiz", email: "pablo@northwind.co", tint: "bg-sky-500" },
  { id: "maya", name: "Maya Chen", email: "maya@northwind.co", tint: "bg-amber-500" },
  { id: "sam", name: "Sam Okafor", email: "sam@northwind.co", tint: "bg-teal-500" },
];

const isFree = (day: string, hour: number) =>
  !events.some((event) => event.day === day && event.start < hour + 1 && event.end > hour);

const clock = (hour: number) => `${((hour + 11) % 12) + 1}${hour < 12 ? " AM" : " PM"}`;
const hm = (hour: number) => {
  const h = ((Math.floor(hour) + 11) % 12) + 1;
  return hour % 1 ? `${h}:${String(Math.round((hour % 1) * 60)).padStart(2, "0")}` : `${h}`;
};
const range = (start: number, end: number) => `${hm(start)} – ${hm(end)}${end < 12 ? "am" : "pm"}`;

type Booking = {
  open: boolean;
  title: string;
  slot: { day: string; hour: number } | null;
  query: string;
  guests: string[];
  sent: boolean;
};

const blank: Booking = { open: false, title: "", slot: null, query: "", guests: [], sent: false };
const named: Booking = { ...blank, open: true, title: "Design review" };

export function AutopilotDemo() {
  const [booking, setBooking] = React.useState(blank);
  const [toast, setToast] = React.useState(false);
  const update = (patch: Partial<Booking>) => setBooking((current) => ({ ...current, ...patch }));

  React.useEffect(() => {
    if (!toast) return;
    const timer = window.setTimeout(() => setToast(false), 4000);
    return () => window.clearTimeout(timer);
  }, [toast]);

  const chapters: AutopilotChapter[] = [
    {
      title: "Draft",
      setup: () => {
        setBooking(blank);
        setToast(false);
      },
      steps: [
        { move: "new-event", say: "Booking a design review" },
        { click: "new-event" },
        { wait: 350 },
        { type: "Design review", into: "title", say: "Giving it a name" },
      ],
    },
    {
      title: "Schedule",
      setup: () => {
        setBooking(named);
        setToast(false);
      },
      steps: [
        { move: "calls", at: [0.55, 0.7], say: "Finding a free slot…" },
        { wait: 450 },
        { click: "slot-tue-15", say: "Tuesday at 3 is free" },
        { wait: 300 },
      ],
    },
    {
      title: "Invite",
      setup: () => {
        setBooking({ ...named, slot: { day: "tue", hour: 15 } });
        setToast(false);
      },
      steps: [
        { type: "pri", into: "guests", say: "Adding Priya" },
        { click: "guest-priya" },
        { click: "send", say: "Sending the invite" },
        { wait: 1400, say: "Booked for Tuesday at 3" },
      ],
    },
  ];

  const matches = booking.query
    ? people.filter(
        (person) =>
          !booking.guests.includes(person.id) &&
          person.name.toLowerCase().includes(booking.query.toLowerCase()),
      )
    : [];
  const slotDay = days.find((day) => day.key === booking.slot?.day);
  const canSend = booking.title.trim() !== "" && booking.slot !== null;

  return (
    <Autopilot
      cursors={[{ id: "agent", label: "Agent", color: "#6d5efc" }]}
      chapters={chapters}
      park="bottom-left"
      className="w-full max-w-3xl"
      stageClassName="overflow-hidden rounded-xl border border-zinc-200 bg-white text-zinc-900 shadow-sm dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-100"
    >
      <div className="relative flex h-[28rem] flex-col">
        {/* Toolbar */}
        <div className="flex h-12 shrink-0 items-center gap-3 border-b border-zinc-200 px-3 sm:px-4 dark:border-white/10">
          <div className="flex items-center gap-2">
            <span className="grid size-6 place-items-center rounded-md bg-zinc-900 text-[10px] font-semibold text-white dark:bg-white dark:text-zinc-900">
              14
            </span>
            <span className="text-sm font-semibold">Calendar</span>
          </div>
          <div className="ml-2 hidden items-center gap-1 text-sm text-zinc-500 sm:flex dark:text-zinc-400">
            <span className="grid size-6 place-items-center rounded-md">‹</span>
            <span className="font-medium text-zinc-800 dark:text-zinc-200">Oct 13 – 17, 2026</span>
            <span className="grid size-6 place-items-center rounded-md">›</span>
          </div>
          <button
            type="button"
            data-autopilot="new-event"
            onClick={() => update({ open: true, sent: false })}
            className="ml-auto inline-flex h-8 items-center gap-1.5 rounded-lg bg-zinc-900 px-3 text-xs font-medium text-white transition-[background-color,scale] duration-150 hover:bg-zinc-700 active:scale-[0.97] data-[autopilot-hover]:bg-zinc-700 data-[autopilot-press]:scale-[0.97] dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200 dark:data-[autopilot-hover]:bg-zinc-200"
          >
            <span aria-hidden className="text-sm leading-none">
              +
            </span>
            New event
          </button>
        </div>

        {/* Week */}
        <div className="flex min-h-0 flex-1 flex-col">
          <div className="flex h-10 shrink-0 border-b border-zinc-200 dark:border-white/10">
            <div className="w-11 shrink-0 sm:w-14" />
            {days.map((day) => (
              <div
                key={day.key}
                className={`flex-1 items-baseline gap-1.5 border-l border-zinc-100 px-2 pt-2.5 text-xs dark:border-white/5 ${day.wide ? "hidden sm:flex" : "flex"}`}
              >
                <span className="text-zinc-500 dark:text-zinc-400">{day.label}</span>
                <span
                  className={
                    day.key === "mon"
                      ? "grid size-5 place-items-center rounded-full bg-rose-500 text-[11px] font-semibold text-white"
                      : "font-semibold"
                  }
                >
                  {day.date}
                </span>
              </div>
            ))}
          </div>
          <div className="relative flex flex-1 overflow-hidden">
            <div className="w-11 shrink-0 sm:w-14">
              {hours.map((hour) => (
                <div
                  key={hour}
                  style={{ height: ROW }}
                  className="pr-2 text-right text-[10px] text-zinc-400 tabular-nums dark:text-zinc-500"
                >
                  <span className="relative -top-1.5">{hour === 9 ? "" : clock(hour)}</span>
                </div>
              ))}
            </div>
            {days.map((day) => (
              <div
                key={day.key}
                className={`relative flex-1 border-l border-zinc-100 dark:border-white/5 ${day.wide ? "hidden sm:block" : "block"}`}
              >
                {hours.map((hour) => (
                  <div
                    key={hour}
                    style={{ top: (hour - 9) * ROW, height: ROW }}
                    className="absolute inset-x-0 border-t border-zinc-100 dark:border-white/5"
                  >
                    {isFree(day.key, hour) && (
                      <button
                        type="button"
                        data-autopilot={`slot-${day.key}-${hour}`}
                        aria-label={`${day.label} ${day.date}, ${clock(hour)}`}
                        onClick={() => update({ open: true, slot: { day: day.key, hour }, sent: false })}
                        className="group/slot absolute inset-0.5 rounded-md text-[10px] font-medium text-violet-600 transition-colors hover:bg-violet-50 data-[autopilot-hover]:bg-violet-50 dark:text-violet-300 dark:hover:bg-violet-400/10 dark:data-[autopilot-hover]:bg-violet-400/10"
                      >
                        <span className="opacity-0 transition-opacity group-hover/slot:opacity-100 group-data-[autopilot-hover]/slot:opacity-100">
                          + {clock(hour)}
                        </span>
                      </button>
                    )}
                  </div>
                ))}
                {day.key === "mon" && (
                  <div aria-hidden style={{ top: 1.35 * ROW }} className="absolute inset-x-0 z-[1] h-px bg-rose-500">
                    <span className="absolute -top-1 -left-1 size-2 rounded-full bg-rose-500" />
                  </div>
                )}
                {events
                  .filter((event) => event.day === day.key)
                  .map((event) => (
                    <div
                      key={event.id}
                      data-autopilot={event.id}
                      style={{ top: (event.start - 9) * ROW + 2, height: (event.end - event.start) * ROW - 4 }}
                      className={`absolute inset-x-1 overflow-hidden rounded-md border-l-2 px-2 text-[11px] leading-4 transition-shadow data-[autopilot-hover]:ring-2 data-[autopilot-hover]:ring-current/15 ${event.end - event.start < 1 ? "py-0" : "py-1"} ${tones[event.tone]}`}
                    >
                      <p className="truncate font-medium">{event.title}</p>
                      {event.end - event.start >= 1 && (
                        <p className="truncate opacity-70 tabular-nums">{range(event.start, event.end)}</p>
                      )}
                    </div>
                  ))}
                {booking.slot?.day === day.key && (
                  <div
                    style={{ top: (booking.slot.hour - 9) * ROW + 2, height: ROW * 1 - 4 }}
                    className={`absolute inset-x-1 overflow-hidden rounded-md px-2 py-1 text-[11px] leading-tight transition-colors duration-300 ${
                      booking.sent
                        ? "bg-violet-600 text-white"
                        : "border border-dashed border-violet-400 bg-violet-50 text-violet-800 dark:bg-violet-400/15 dark:text-violet-100"
                    }`}
                  >
                    <p className="truncate font-medium">{booking.title || "(No title)"}</p>
                  </div>
                )}
              </div>
            ))}
          </div>
        </div>

        {/* New event panel */}
        <div
          inert={!booking.open}
          aria-hidden={!booking.open}
          className={`absolute top-12 right-0 bottom-0 z-10 flex w-52 flex-col border-l border-zinc-200 bg-white shadow-[-12px_0_32px_-16px_rgb(0_0_0/0.18)] transition-transform duration-300 ease-out sm:w-72 dark:border-white/10 dark:bg-zinc-900 ${
            booking.open ? "translate-x-0" : "translate-x-full"
          }`}
        >
          <div className="flex items-center justify-between px-4 pt-4">
            <p className="text-xs font-medium tracking-wide text-zinc-500 uppercase dark:text-zinc-400">
              New event
            </p>
            <button
              type="button"
              aria-label="Close"
              onClick={() => update({ open: false })}
              className="grid size-6 place-items-center rounded-md text-zinc-400 hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-white/10 dark:hover:text-zinc-200"
            >
              ×
            </button>
          </div>
          <div className="flex flex-1 flex-col gap-4 px-4 pt-3 pb-4">
            <input
              data-autopilot="title"
              value={booking.title}
              onChange={(event) => update({ title: event.target.value })}
              placeholder="Add title"
              aria-label="Title"
              className="h-10 w-full rounded-lg border border-zinc-200 bg-transparent px-3 text-base font-medium outline-none placeholder:text-zinc-400 focus:border-violet-500 focus:ring-3 focus:ring-violet-500/20 data-[autopilot-focus]:border-violet-500 data-[autopilot-focus]:ring-3 data-[autopilot-focus]:ring-violet-500/20 sm:text-sm dark:border-white/10"
            />
            <div className="flex items-start gap-2.5 text-sm">
              <ClockIcon />
              {booking.slot && slotDay ? (
                <p>
                  {slotDay.label}, Oct {slotDay.date}
                  <span className="block text-xs text-zinc-500 dark:text-zinc-400">
                    {clock(booking.slot.hour)} – {clock(booking.slot.hour + 1)}
                  </span>
                </p>
              ) : (
                <p className="text-zinc-400 dark:text-zinc-500">Pick a time on the calendar</p>
              )}
            </div>
            <div className="flex items-start gap-2.5 text-sm">
              <PeopleIcon />
              <div className="relative min-w-0 flex-1">
                {booking.guests.length > 0 && (
                  <div className="mb-2 flex flex-wrap gap-1.5">
                    {booking.guests.map((id) => {
                      const person = people.find((p) => p.id === id)!;
                      return (
                        <span
                          key={id}
                          className="inline-flex items-center gap-1.5 rounded-full bg-zinc-100 py-0.5 pr-2 pl-0.5 text-xs dark:bg-white/10"
                        >
                          <Avatar person={person} />
                          {person.name}
                        </span>
                      );
                    })}
                  </div>
                )}
                <input
                  data-autopilot="guests"
                  value={booking.query}
                  onChange={(event) => update({ query: event.target.value })}
                  placeholder="Add guests"
                  aria-label="Add guests"
                  className="h-8 w-full rounded-lg border border-zinc-200 bg-transparent px-2.5 text-base outline-none placeholder:text-zinc-400 focus:border-violet-500 focus:ring-3 focus:ring-violet-500/20 data-[autopilot-focus]:border-violet-500 data-[autopilot-focus]:ring-3 data-[autopilot-focus]:ring-violet-500/20 sm:text-sm dark:border-white/10"
                />
                {matches.length > 0 && (
                  <ul className="absolute inset-x-0 top-full z-10 mt-1 overflow-hidden rounded-lg border border-zinc-200 bg-white p-1 shadow-lg dark:border-white/10 dark:bg-zinc-800">
                    {matches.map((person) => (
                      <li key={person.id}>
                        <button
                          type="button"
                          data-autopilot={`guest-${person.id}`}
                          onClick={() => update({ guests: [...booking.guests, person.id], query: "" })}
                          className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left hover:bg-zinc-100 data-[autopilot-hover]:bg-zinc-100 dark:hover:bg-white/10 dark:data-[autopilot-hover]:bg-white/10"
                        >
                          <Avatar person={person} />
                          <span className="min-w-0">
                            <span className="block truncate text-xs font-medium">{person.name}</span>
                            <span className="block truncate text-[11px] text-zinc-500 dark:text-zinc-400">
                              {person.email}
                            </span>
                          </span>
                        </button>
                      </li>
                    ))}
                  </ul>
                )}
              </div>
            </div>
            <button
              type="button"
              data-autopilot="send"
              disabled={!canSend}
              onClick={() => {
                update({ open: false, sent: true, query: "" });
                setToast(true);
              }}
              className="mt-auto h-9 rounded-lg bg-violet-600 text-sm font-medium text-white transition-[background-color,scale,opacity] duration-150 hover:bg-violet-500 active:scale-[0.98] disabled:opacity-40 data-[autopilot-hover]:bg-violet-500 data-[autopilot-press]:scale-[0.98]"
            >
              Send invite
            </button>
          </div>
        </div>

        {/* Toast */}
        <div
          role="status"
          className={`pointer-events-none absolute top-14 left-1/2 z-20 flex -translate-x-1/2 items-center gap-2 rounded-full bg-zinc-900 py-1.5 pr-3.5 pl-2 text-xs whitespace-nowrap text-white shadow-lg transition-[opacity,translate] duration-300 dark:bg-white dark:text-zinc-900 ${
            toast ? "translate-y-0 opacity-100" : "-translate-y-2 opacity-0"
          }`}
        >
          {toast && (
            <>
              <span className="grid size-4 place-items-center rounded-full bg-emerald-500 text-[10px] text-white">✓</span>
              Invite sent to {booking.guests.length ? people.find((p) => p.id === booking.guests[0])?.name : "your guests"}
            </>
          )}
        </div>
      </div>
    </Autopilot>
  );
}

function Avatar({ person }: { person: (typeof people)[number] }) {
  return (
    <span
      aria-hidden
      className={`grid size-5 shrink-0 place-items-center rounded-full text-[9px] font-semibold text-white ${person.tint}`}
    >
      {person.name
        .split(" ")
        .map((part) => part[0])
        .join("")}
    </span>
  );
}

function ClockIcon() {
  return (
    <svg viewBox="0 0 16 16" className="mt-0.5 size-4 shrink-0 text-zinc-400" fill="none" aria-hidden>
      <circle cx="8" cy="8" r="6.25" stroke="currentColor" strokeWidth="1.5" />
      <path d="M8 4.75V8l2.25 1.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
    </svg>
  );
}

function PeopleIcon() {
  return (
    <svg viewBox="0 0 16 16" className="mt-2 size-4 shrink-0 text-zinc-400" fill="none" aria-hidden>
      <circle cx="6" cy="5.5" r="2.5" stroke="currentColor" strokeWidth="1.5" />
      <path d="M1.75 13c.5-2.2 2.2-3.5 4.25-3.5S9.75 10.8 10.25 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
      <path d="M10.5 3.2a2.4 2.4 0 0 1 0 4.6M12 9.7c1.2.5 2 1.6 2.25 3.3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
    </svg>
  );
}
```
