{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tour-1",
  "title": "Tour 1: guided product tour",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A guided product tour: a spotlight lifts one part of your interface out of a dimmed page while a card says what it is for, with Next, Back, arrow keys, Escape, a progress bar, focus that follows the step, and a restart.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/tour-1/tour-1.tsx",
      "content": "// Ballmac UI: Tour 1. https://ui.ballmac.com/blocks/tour-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowLeft, ArrowRight, Bell, BarChart3, FolderKanban, Home, Inbox, Play, RotateCcw, Search, Settings, X } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { cn } from \"@/lib/utils\"\n\ntype Tour1Step = {\n  /** The value of the data-tour attribute on the element to point at. */\n  target: string\n  title: string\n  body: string\n}\n\ntype Tour1Props = Omit<React.ComponentProps<\"section\">, \"title\" | \"onChange\"> & {\n  steps?: Tour1Step[]\n  /** The interface to tour. Mark the elements to point at with data-tour=\"name\". Leave out to tour a built-in sample app. */\n  children?: React.ReactNode\n  /** Start the tour already running, at a step. */\n  defaultStep?: number | null\n  /** Called when the tour ends, finished or skipped. */\n  onFinish?: (result: { completed: boolean; step: number }) => void\n}\n\nconst sampleSteps: Tour1Step[] = [\n  { target: \"nav\", title: \"Everything starts here\", body: \"Move between your home, projects, inbox and reports. The one you are on is highlighted.\" },\n  { target: \"search\", title: \"Find anything fast\", body: \"Press / from anywhere to search projects, people and files. It finds words inside documents too.\" },\n  { target: \"chart\", title: \"See how the week went\", body: \"This chart shows work finished each day. Hover or focus a bar to read its exact number.\" },\n  { target: \"alerts\", title: \"Never miss a mention\", body: \"Replies and approvals land here. A dot means something is waiting for you.\" },\n]\n\nfunction SampleApp() {\n  const bars = [38, 52, 44, 68, 59, 31, 74]\n  return (\n    <div className=\"bg-card grid min-h-[36rem] grid-cols-[3.5rem_minmax(0,1fr)] sm:grid-cols-[11rem_minmax(0,1fr)]\">\n      <nav data-tour=\"nav\" aria-label=\"Sample navigation\" className=\"bg-muted/40 border-e p-2 sm:p-3\">\n        <p className=\"mb-3 hidden px-2 text-sm font-semibold sm:block\">Acme</p>\n        {[[Home, \"Home\"], [FolderKanban, \"Projects\"], [Inbox, \"Inbox\"], [BarChart3, \"Reports\"], [Settings, \"Settings\"]].map(([Icon, label], i) => {\n          const I = Icon as typeof Home\n          return (\n            <div key={label as string} className={cn(\"flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm\", i === 0 ? \"bg-background font-medium shadow-sm\" : \"text-muted-foreground\")}>\n              <I aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n              <span className=\"hidden sm:inline\">{label as string}</span>\n            </div>\n          )\n        })}\n      </nav>\n      <div className=\"min-w-0 p-3 sm:p-5\">\n        <div className=\"flex items-center gap-3\">\n          <div data-tour=\"search\" className=\"bg-background text-muted-foreground flex h-9 min-w-0 flex-1 items-center gap-2 rounded-lg border px-3 text-sm\">\n            <Search aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n            <span className=\"truncate\">Search projects, people, files</span>\n          </div>\n          <div data-tour=\"alerts\" className=\"bg-background relative flex size-9 shrink-0 items-center justify-center rounded-lg border\">\n            <Bell aria-hidden=\"true\" className=\"size-4\" />\n            <span aria-hidden=\"true\" className=\"bg-destructive absolute end-1.5 top-1.5 size-2 rounded-full\" />\n          </div>\n        </div>\n        <div className=\"mt-4 grid grid-cols-3 gap-3\">\n          {[[\"Open\", \"24\"], [\"Done\", \"118\"], [\"Late\", \"3\"]].map(([l, v]) => (\n            <div key={l} className=\"rounded-xl border p-3\">\n              <p className=\"text-muted-foreground text-xs\">{l}</p>\n              <p className=\"text-xl font-semibold tabular-nums\">{v}</p>\n            </div>\n          ))}\n        </div>\n        <div data-tour=\"chart\" className=\"mt-4 rounded-xl border p-4\">\n          <p className=\"text-sm font-medium\">Finished this week</p>\n          <div className=\"mt-3 flex h-28 items-end gap-2\" aria-hidden=\"true\">\n            {bars.map((h, i) => (\n              <span key={i} className=\"bg-chart-1/70 flex-1 rounded-t-md\" style={{ height: `${h}%` }} />\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\n/**\n * A guided product tour: a spotlight lifts one part of your interface out of a dimmed page while a card beside it says what it is for. Next, Back, the arrow\n * keys and Escape all work, a progress bar and 'Step 2 of 4' say where you are, focus moves to the card at each step, and the tour can be started again. Mark what to\n * point at with data-tour attributes in your own interface, or leave it empty to tour the sample app.\n */\nfunction Tour1({ steps = sampleSteps, children, defaultStep = null, onFinish, className, ...props }: Tour1Props) {\n  const id = React.useId()\n  const [step, setStep] = React.useState<number | null>(defaultStep)\n  const [rect, setRect] = React.useState<{ x: number; y: number; w: number; h: number } | null>(null)\n  const [box, setBox] = React.useState({ w: 0, h: 0 })\n  const stage = React.useRef<HTMLDivElement>(null)\n  const card = React.useRef<HTMLDivElement>(null)\n  const startBtn = React.useRef<HTMLButtonElement>(null)\n  const running = step !== null\n  const s = running ? steps[step!] : undefined\n\n  const measure = React.useCallback(() => {\n    const root = stage.current\n    if (!root || step === null) return\n    const el = root.querySelector<HTMLElement>(`[data-tour=\"${steps[step]?.target}\"]`)\n    const r0 = root.getBoundingClientRect()\n    setBox({ w: r0.width, h: r0.height })\n    if (!el) return setRect(null)\n    const r = el.getBoundingClientRect()\n    setRect({ x: r.left - r0.left, y: r.top - r0.top, w: r.width, h: r.height })\n  }, [step, steps])\n\n  React.useLayoutEffect(() => {\n    measure()\n    if (!running) return\n    window.addEventListener(\"resize\", measure)\n    const ro = typeof ResizeObserver !== \"undefined\" ? new ResizeObserver(measure) : null\n    if (stage.current) ro?.observe(stage.current)\n    return () => {\n      window.removeEventListener(\"resize\", measure)\n      ro?.disconnect()\n    }\n  }, [measure, running])\n\n  // Move focus to the card when a step appears, and back to Start when the tour ends.\n  const wasRunning = React.useRef(running)\n  const mounted = React.useRef(false)\n  React.useEffect(() => {\n    // Never take focus on page load, only when the person starts, moves or ends the tour.\n    if (mounted.current) {\n      if (running) card.current?.querySelector<HTMLElement>(\"[data-primary]\")?.focus({ preventScroll: true })\n      else if (wasRunning.current) startBtn.current?.focus()\n    }\n    mounted.current = true\n    wasRunning.current = running\n  }, [running, step])\n\n  function end(completed: boolean) {\n    onFinish?.({ completed, step: step ?? 0 })\n    setStep(null)\n  }\n  const go = (d: number) => {\n    if (step === null) return\n    const n = step + d\n    if (n >= steps.length) end(true)\n    else if (n >= 0) setStep(n)\n  }\n\n  // The card sits below the target when there is room, otherwise above, and stays inside the stage.\n  const CARD_W = Math.min(320, Math.max(220, box.w - 24))\n  const pad = 8\n  const CARD_H = 215\n  const pos = (() => {\n    if (!rect) return { left: 12, top: 12 }\n    const below = rect.y + rect.h + pad + 6 + CARD_H < box.h\n    const left = Math.max(12, Math.min(box.w - CARD_W - 12, rect.x + rect.w / 2 - CARD_W / 2))\n    return { left, top: Math.max(12, Math.min(box.h - CARD_H - 12, below ? rect.y + rect.h + pad + 6 : rect.y - pad - CARD_H - 6)) }\n  })()\n\n  return (\n    <section data-slot=\"tour-1\" aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-4xl px-4 py-10 sm:px-6\", className)} {...props}>\n      <div className=\"mb-6 flex flex-wrap items-end justify-between gap-4\">\n        <div>\n          <h2 id={`${id}-title`} className=\"text-3xl font-semibold tracking-[-0.035em] text-balance\">\n            Take the tour\n          </h2>\n          <p className=\"text-muted-foreground mt-2 max-w-md text-pretty\">A minute to see where things are.</p>\n        </div>\n        <Button ref={startBtn} onClick={() => setStep(0)} disabled={running}>\n          {running ? <RotateCcw aria-hidden=\"true\" /> : <Play aria-hidden=\"true\" className=\"fill-current\" />}\n          {running ? \"Tour in progress\" : \"Start the tour\"}\n        </Button>\n      </div>\n\n      <div\n        ref={stage}\n        className=\"relative overflow-hidden rounded-3xl border\"\n        onKeyDown={(e) => {\n          if (!running) return\n          if (e.key === \"Escape\") { e.stopPropagation(); end(false) }\n          else if (e.key === \"ArrowRight\") { e.preventDefault(); go(1) }\n          else if (e.key === \"ArrowLeft\") { e.preventDefault(); go(-1) }\n        }}\n      >\n        <div inert={running ? true : undefined}>{children ?? <SampleApp />}</div>\n\n        {running && s ? (\n          <>\n            {!rect ? <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 bg-black/55\" /> : null}\n            {rect ? (\n              <div aria-hidden=\"true\" className=\"ring-background pointer-events-none absolute rounded-xl ring-2 transition-[left,top,width,height] duration-300 ease-out motion-reduce:transition-none\" style={{ left: rect.x - 6, top: rect.y - 6, width: rect.w + 12, height: rect.h + 12, boxShadow: \"0 0 0 9999px rgb(0 0 0 / 0.55)\" }} />\n            ) : null}\n            <div\n              ref={card}\n              role=\"dialog\"\n              aria-modal=\"false\"\n              aria-labelledby={`${id}-st`}\n              aria-describedby={`${id}-sb`}\n              className=\"bg-popover text-popover-foreground absolute z-10 rounded-2xl border p-5 shadow-2xl transition-[left,top] duration-300 ease-out motion-reduce:transition-none\"\n              style={{ width: CARD_W, left: pos.left, top: pos.top }}\n            >\n              <div className=\"flex items-start justify-between gap-3\">\n                <p className=\"text-muted-foreground text-xs font-medium tabular-nums\">\n                  Step {step! + 1} of {steps.length}\n                </p>\n                <button type=\"button\" onClick={() => end(false)} aria-label=\"Skip the tour\" className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 -m-1 rounded-md p-1 outline-none focus-visible:ring-[3px]\">\n                  <X aria-hidden=\"true\" className=\"size-4\" />\n                </button>\n              </div>\n              <h3 id={`${id}-st`} className=\"mt-1.5 font-semibold tracking-tight text-balance\">\n                {s.title}\n              </h3>\n              <p id={`${id}-sb`} className=\"text-muted-foreground mt-1.5 text-sm text-pretty\">\n                {s.body}\n              </p>\n              <div role=\"progressbar\" aria-label=\"Tour progress\" aria-valuemin={1} aria-valuemax={steps.length} aria-valuenow={step! + 1} className=\"mt-4 flex gap-1\">\n                {steps.map((_, i) => (\n                  <span key={i} aria-hidden=\"true\" className={cn(\"h-1 flex-1 rounded-full\", i <= step! ? \"bg-foreground\" : \"bg-muted\")} />\n                ))}\n              </div>\n              <div className=\"mt-4 flex items-center justify-between gap-2\">\n                <Button variant=\"ghost\" size=\"sm\" onClick={() => go(-1)} disabled={step === 0}>\n                  <ArrowLeft aria-hidden=\"true\" className=\"rtl:rotate-180\" />\n                  Back\n                </Button>\n                <Button size=\"sm\" data-primary onClick={() => go(1)}>\n                  {step === steps.length - 1 ? \"Finish\" : \"Next\"}\n                  {step === steps.length - 1 ? null : <ArrowRight aria-hidden=\"true\" className=\"rtl:rotate-180\" />}\n                </Button>\n              </div>\n            </div>\n          </>\n        ) : null}\n      </div>\n      <p className=\"text-muted-foreground mt-3 text-xs\">While the tour runs: Left and Right arrows move between steps, Escape ends it.</p>\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {running && s ? `Step ${step! + 1} of ${steps.length}: ${s.title}` : \"\"}\n      </p>\n    </section>\n  )\n}\n\nexport { Tour1, type Tour1Props, type Tour1Step }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/tour-1/tour-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "product tour",
      "onboarding",
      "walkthrough",
      "tooltips",
      "coachmarks",
      "spotlight",
      "guide",
      "tutorial",
      "first run"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "A product tour. <Tour1 steps={[{ target: 'search', title, body }]} defaultStep={null | 0} onFinish={({ completed, step }) => ...}>{your interface}</Tour1>. Mark the elements to point at with data-tour='search' inside the children; leave children out to tour a built-in sample app. The page behind is inert while the tour runs; the spotlight and card follow each target and the card stays inside the stage, above or below the target.",
      "whenToUse": [
        "First-run onboarding that points at real parts of the interface",
        "Introducing a redesign or a new feature"
      ],
      "whenNotToUse": [
        "A setup checklist (use onboarding-1)",
        "One-off hints (use tooltip or popover)"
      ],
      "composesWith": [
        "onboarding-1",
        "app-shell-1",
        "dashboard-1"
      ],
      "a11y": [
        {
          "keys": "Left / Right, Escape",
          "action": "Move between steps and end the tour; Next, Back and Skip are real buttons and the card is a dialog named by its title"
        },
        {
          "keys": "Focus",
          "action": "Moves to the card's Next button at every step, and back to Start the tour when it ends; the page behind is inert, so focus cannot wander into it"
        },
        {
          "keys": "Progress",
          "action": "'Step 2 of 4' in text, a labelled progress bar, and each step announced politely"
        }
      ],
      "customization": [
        "Targets are found by data-tour attributes, so any interface can be toured",
        "The spotlight and card animate between steps and jump with reduced motion",
        "The card is placed below the target when there is room, otherwise above, and clamped inside the stage"
      ]
    },
    "statesNone": "A guided overlay with no data of its own: nothing to load, submit or fail.",
    "examples": [
      "tour-1-demo",
      "tour-1-running"
    ],
    "url": "https://ui.ballmac.com/components/tour-1"
  },
  "categories": [
    "blocks",
    "tour"
  ],
  "type": "registry:block"
}