# Autopilot: Three Clicks

> A SaaS app sets up a project in three clicks, one chapter each, with `chapterStrip` showing Create, Invite and Ship underneath. Click a chapter to jump there, or pause from the strip. On a phone, tap the app to take over and press Resume to hand it back.

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

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/autopilot-onboarding-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/autopilot-onboarding-demo-light-c29c54458642ec07.png
- Registry item: https://keyframeui.com/r/autopilot-onboarding-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-onboarding-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-onboarding-demo

## Files

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

```tsx
"use client";

import * as React from "react";

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

const templates = [
  { id: "website", name: "Website", note: "Marketing site with a blog", tint: "from-sky-400 to-indigo-500" },
  { id: "docs", name: "Docs", note: "Searchable product docs", tint: "from-amber-300 to-orange-500" },
  { id: "store", name: "Store", note: "Catalog and checkout", tint: "from-emerald-300 to-teal-600" },
];

const teammates = [
  { name: "Sam Okafor", tint: "bg-teal-500" },
  { name: "Priya Raman", tint: "bg-rose-500" },
  { name: "Leo Park", tint: "bg-sky-500" },
];

type Stage = "empty" | "created" | "invited" | "building" | "live";
const order: Stage[] = ["empty", "created", "invited", "building", "live"];
const reached = (stage: Stage, step: Stage) => order.indexOf(stage) >= order.indexOf(step);

export function AutopilotOnboardingDemo() {
  const [stage, setStage] = React.useState<Stage>("empty");
  const [template, setTemplate] = React.useState("website");

  // Publishing takes a moment, as a real build would.
  React.useEffect(() => {
    if (stage !== "building") return;
    const timer = window.setTimeout(() => setStage("live"), 1100);
    return () => window.clearTimeout(timer);
  }, [stage]);

  const chapters: AutopilotChapter[] = [
    {
      title: "Create",
      setup: () => setStage("empty"),
      steps: [
        { move: "template-website", say: "1 · Pick a template" },
        { click: "template-website" },
        { wait: 900, say: "Project ready" },
      ],
    },
    {
      title: "Invite",
      setup: () => {
        setTemplate("website");
        setStage("created");
      },
      steps: [
        { move: "invite", say: "2 · Bring your team" },
        { click: "invite" },
        { wait: 1000, say: "Three teammates in" },
      ],
    },
    {
      title: "Ship",
      setup: () => {
        setTemplate("website");
        setStage("invited");
      },
      steps: [
        { move: "publish", say: "3 · Ship it" },
        { click: "publish" },
        { wait: 1800, say: "Live in one click" },
      ],
    },
  ];

  const picked = templates.find((item) => item.id === template) ?? templates[0];

  return (
    <Autopilot
      cursors={[{ id: "guide", label: "Guide", color: "#0f766e" }]}
      chapters={chapters}
      chapterStrip
      className="w-full max-w-3xl text-zinc-900 dark:text-zinc-100"
      stageClassName="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm dark:border-white/10 dark:bg-zinc-900"
    >
      <div className="flex h-[24rem]">
        {/* Sidebar */}
        <aside className="hidden w-44 shrink-0 flex-col border-r border-zinc-200 bg-zinc-50 p-3 sm:flex dark:border-white/10 dark:bg-white/[0.02]">
          <div className="flex items-center gap-2 px-1.5 py-1">
            <span className="grid size-6 place-items-center rounded-md bg-teal-700 text-[11px] font-bold text-white">N</span>
            <span className="text-sm font-semibold">Northwind</span>
          </div>
          <nav className="mt-4 flex flex-col gap-0.5 text-[13px]">
            {["Projects", "Deployments", "Team", "Settings"].map((item, i) => (
              <span
                key={item}
                className={`rounded-md px-2 py-1.5 ${i === 0 ? "bg-white font-medium shadow-sm ring-1 ring-zinc-200 dark:bg-white/10 dark:ring-white/10" : "text-zinc-500 dark:text-zinc-400"}`}
              >
                {item}
              </span>
            ))}
          </nav>
          <div className="mt-auto flex items-center gap-2 px-1.5 text-xs text-zinc-500 dark:text-zinc-400">
            <span className="grid size-6 place-items-center rounded-full bg-zinc-800 text-[10px] font-semibold text-white dark:bg-zinc-600">
              A
            </span>
            Ana · Acme
          </div>
        </aside>

        <main className="flex min-w-0 flex-1 flex-col">
          <header className="flex h-12 shrink-0 items-center gap-2 border-b border-zinc-200 px-4 dark:border-white/10">
            <p className="truncate text-sm">
              <span className="text-zinc-400">Projects</span>
              {stage !== "empty" && <span className="font-medium"> / {picked.name}</span>}
            </p>
            {stage !== "empty" && (
              <span
                className={`ml-1 inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium ${
                  stage === "live"
                    ? "bg-emerald-50 text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-300"
                    : "bg-zinc-100 text-zinc-600 dark:bg-white/10 dark:text-zinc-300"
                }`}
              >
                <span
                  className={`size-1.5 rounded-full ${stage === "live" ? "bg-emerald-500" : stage === "building" ? "animate-pulse bg-amber-500" : "bg-zinc-400"}`}
                />
                {stage === "live" ? "Live" : stage === "building" ? "Building" : "Draft"}
              </span>
            )}
            <div className="ml-auto flex items-center gap-2">
              {reached(stage, "invited") && (
                <div className="flex -space-x-1.5">
                  {teammates.map((person) => (
                    <span
                      key={person.name}
                      title={person.name}
                      className={`grid size-6 place-items-center rounded-full text-[9px] font-semibold text-white ring-2 ring-white dark:ring-zinc-900 ${person.tint}`}
                    >
                      {initials(person.name)}
                    </span>
                  ))}
                </div>
              )}
              {stage !== "empty" && (
                <button
                  type="button"
                  data-autopilot="publish"
                  disabled={stage === "building"}
                  onClick={() => setStage(stage === "live" ? "invited" : "building")}
                  className="inline-flex h-8 items-center 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] disabled:opacity-60 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"
                >
                  {stage === "live" ? "Unpublish" : stage === "building" ? "Publishing…" : "Publish"}
                </button>
              )}
            </div>
          </header>

          {stage === "empty" ? (
            <div className="flex flex-1 flex-col justify-center px-4 py-6 sm:px-8">
              <p className="text-lg font-semibold tracking-tight">Start a project</p>
              <p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Pick a template. You can change everything later.</p>
              <div className="mt-5 grid grid-cols-3 gap-2.5 sm:gap-3">
                {templates.map((item) => (
                  <button
                    key={item.id}
                    type="button"
                    data-autopilot={`template-${item.id}`}
                    onClick={() => {
                      setTemplate(item.id);
                      setStage("created");
                    }}
                    className="group/template rounded-xl border border-zinc-200 p-2 text-left transition-[border-color,box-shadow,scale] duration-150 hover:border-zinc-300 hover:shadow-md active:scale-[0.98] data-[autopilot-hover]:border-zinc-300 data-[autopilot-hover]:shadow-md data-[autopilot-press]:scale-[0.98] dark:border-white/10 dark:hover:border-white/20 dark:data-[autopilot-hover]:border-white/20"
                  >
                    <span className={`block aspect-[4/3] rounded-lg bg-gradient-to-br p-2 ${item.tint}`}>
                      <span className="block h-1.5 w-1/2 rounded-sm bg-white/80" />
                      <span className="mt-1 block h-1 w-1/3 rounded-sm bg-white/50" />
                    </span>
                    <span className="mt-2 block text-[13px] font-medium">{item.name}</span>
                    <span className="hidden text-[11px] text-zinc-500 sm:block dark:text-zinc-400">{item.note}</span>
                  </button>
                ))}
              </div>
            </div>
          ) : (
            <div className="grid flex-1 grid-cols-1 gap-4 overflow-hidden p-4 sm:grid-cols-[1fr_13rem]">
              {/* Preview */}
              <div className="hidden flex-col overflow-hidden rounded-xl border border-zinc-200 sm:flex dark:border-white/10">
                <div className="flex h-7 items-center gap-1.5 border-b border-zinc-200 px-2.5 dark:border-white/10">
                  <span className="size-1.5 rounded-full bg-zinc-300 dark:bg-zinc-600" />
                  <span className="size-1.5 rounded-full bg-zinc-300 dark:bg-zinc-600" />
                  <span className="ml-2 truncate font-mono text-[10px] text-zinc-400">
                    {stage === "live" ? "website-7f3.northwind.app" : "preview"}
                  </span>
                </div>
                <div className={`flex-1 bg-gradient-to-br p-4 ${picked.tint}`}>
                  <span className="block h-2.5 w-2/3 rounded-sm bg-white/90" />
                  <span className="mt-1.5 block h-2.5 w-1/2 rounded-sm bg-white/90" />
                  <span className="mt-3 block h-1.5 w-3/4 rounded-sm bg-white/60" />
                  <span className="mt-1 block h-1.5 w-2/3 rounded-sm bg-white/60" />
                  <span className="mt-4 block h-5 w-16 rounded-full bg-white" />
                </div>
              </div>

              {/* Checklist */}
              <div className="flex flex-col gap-2">
                <p className="text-xs font-medium tracking-wide text-zinc-400 uppercase">Getting started</p>
                <Task done label={`Created from ${picked.name}`} />
                <Task done={reached(stage, "invited")} label={reached(stage, "invited") ? "Team invited" : "Invite your team"}>
                  {!reached(stage, "invited") && (
                    <div className="mt-2 flex items-center gap-2">
                      <div className="flex -space-x-1.5">
                        {teammates.map((person) => (
                          <span
                            key={person.name}
                            className={`grid size-5 place-items-center rounded-full text-[8px] font-semibold text-white ring-2 ring-white dark:ring-zinc-900 ${person.tint}`}
                          >
                            {initials(person.name)}
                          </span>
                        ))}
                      </div>
                      <button
                        type="button"
                        data-autopilot="invite"
                        onClick={() => setStage("invited")}
                        className="ml-auto h-7 rounded-md border border-zinc-200 px-2.5 text-xs font-medium transition-[background-color,scale] duration-150 hover:bg-zinc-50 active:scale-[0.97] data-[autopilot-hover]:bg-zinc-50 data-[autopilot-press]:scale-[0.97] dark:border-white/15 dark:hover:bg-white/5 dark:data-[autopilot-hover]:bg-white/5"
                      >
                        Invite 3
                      </button>
                    </div>
                  )}
                </Task>
                <Task
                  done={stage === "live"}
                  label={stage === "live" ? "Published" : stage === "building" ? "Publishing…" : "Publish"}
                >
                  {stage === "live" && (
                    <p className="mt-1 truncate font-mono text-[10px] text-emerald-600 dark:text-emerald-400">
                      website-7f3.northwind.app
                    </p>
                  )}
                </Task>
              </div>
            </div>
          )}
        </main>
      </div>
    </Autopilot>
  );
}

function Task({ done, label, children }: { done: boolean; label: string; children?: React.ReactNode }) {
  return (
    <div className="rounded-lg border border-zinc-200 p-2.5 text-[13px] dark:border-white/10">
      <p className="flex items-center gap-2">
        <span
          className={`grid size-4 shrink-0 place-items-center rounded-full text-[9px] transition-colors duration-300 ${
            done ? "bg-emerald-500 text-white" : "border border-dashed border-zinc-300 dark:border-zinc-600"
          }`}
        >
          {done && "✓"}
        </span>
        <span className={done ? "text-zinc-500 dark:text-zinc-400" : "font-medium"}>{label}</span>
      </p>
      {children}
    </div>
  );
}

const initials = (name: string) =>
  name
    .split(" ")
    .map((part) => part[0])
    .join("");
```
