{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tour",
  "title": "Tour",
  "description": "A tour of callouts: each step an ink callout hung on a leader line that ends in a dot on the thing it is about, one step at a time, only when you press on.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/dialog.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/tour.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Dialog, DialogSurface, DialogTrigger, useDialog } from \"@/components/ui/dialog\"\n\ntype TourProps = {\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  /** The step you're on, counted from 1. Left uncontrolled, the tour starts again from the first step each time it opens. */\n  value?: number\n  defaultValue?: number\n  onValueChange?: (value: number) => void\n  children: React.ReactNode\n}\n\nconst Place = React.createContext<{ at: number; go: (n: number) => void } | null>(null)\n\n/**\n * A tour of callouts, after Weingart's letter: each step is an ink callout hung on a leader line that ends in a\n * dot on the thing it is about. It moves only when the person presses Next or Back (or the arrow keys); it never\n * plays by itself. Built on the native <dialog>, so focus, Escape and the top layer are the browser's.\n */\nfunction Tour({ open, defaultOpen, onOpenChange, value, defaultValue = 1, onValueChange, children }: TourProps) {\n  const [own, setOwn] = React.useState(defaultValue)\n  const at = value ?? own\n  const go = React.useCallback(\n    (n: number) => {\n      if (value === undefined) setOwn(n)\n      onValueChange?.(n)\n    },\n    [value, onValueChange],\n  )\n  const change = React.useCallback(\n    (next: boolean) => {\n      if (!next && value === undefined) setOwn(defaultValue) // the next tour starts at the beginning\n      onOpenChange?.(next)\n    },\n    [value, defaultValue, onOpenChange],\n  )\n  const place = React.useMemo(() => ({ at, go }), [at, go])\n  return (\n    <Dialog open={open} defaultOpen={defaultOpen} onOpenChange={change}>\n      <Place.Provider value={place}>{children}</Place.Provider>\n    </Dialog>\n  )\n}\n\nconst TourTrigger = DialogTrigger\n\ntype TourStepProps = {\n  /** What the step is about: a selector, or a ref. Left out, the callout stands alone in the middle, with no leader. */\n  target?: string | React.RefObject<Element | null>\n  /** The step's name, in bold at the head of the callout. */\n  title: React.ReactNode\n  /** A sentence or two. */\n  children?: React.ReactNode\n}\n\n/** One step. It renders nothing itself: TourContent reads it and shows the one you're on. */\nfunction TourStep(props: TourStepProps): React.ReactNode {\n  void props\n  return null\n}\n\ntype TourContentProps = Omit<React.ComponentProps<\"dialog\">, \"children\"> & {\n  /** The steps, as TourStep elements, in order. */\n  children: React.ReactNode\n  /** The words on the callout's controls. */\n  labels?: { back?: string; next?: string; done?: string; end?: string }\n}\n\nconst two = (n: number) => String(n).padStart(2, \"0\")\nconst clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), Math.max(lo, hi))\n\n/** The window-wide, see-through dialog that holds the leader and the callout of the step you're on. */\nfunction TourContent({ className, children, labels, onKeyDown, ...props }: TourContentProps) {\n  const { open, setOpen, titleId, descriptionId } = useDialog()\n  const place = React.useContext(Place)\n  if (!place) throw new Error(\"TourContent must sit inside <Tour>.\")\n  const steps = React.Children.toArray(children)\n    .filter(React.isValidElement<TourStepProps>)\n    .map((kid) => kid.props)\n  const n = steps.length\n  const at = clamp(place.at, 1, n)\n  const step = steps[at - 1]\n  const { back = \"Back\", next = \"Next\", done = \"Done\", end = \"End\" } = labels ?? {}\n  const countId = `${titleId}-count`\n  const callout = React.useRef<HTMLDivElement>(null)\n  const lead = React.useRef<SVGSVGElement>(null)\n  const moved = React.useRef<\"next\" | \"back\" | null>(null)\n  const target = step?.target\n\n  const move = (to: number) => {\n    if (to > n) return setOpen(false)\n    if (to < 1 || to === at) return\n    moved.current = to > at ? \"next\" : \"back\"\n    place.go(to)\n  }\n\n  // Hang the callout: the dot on the target's edge, the leader leaning off it toward the roomier side, the callout\n  // a gap beyond, held inside the window. Written straight to the DOM, so the first frame is already in place.\n  React.useEffect(() => {\n    const box = callout.current\n    const svg = lead.current\n    const d = box?.closest(\"dialog\")\n    if (!open || !box || !svg || !d) return\n    const el = typeof target === \"string\" ? document.querySelector(target) : (target?.current ?? null)\n    const still = matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    const line = svg.querySelector(\"line\")\n    const dot = svg.querySelector(\"circle\")\n    const margin = 20\n    const lay = () => {\n      const space = getComputedStyle(d).getPropertyValue(\"--ot-space-7\").trim() // 3.75rem, as the tokens are written\n      const gap = parseFloat(space) * (space.endsWith(\"rem\") ? parseFloat(getComputedStyle(document.documentElement).fontSize) : 1) || 60\n      const vw = d.clientWidth\n      const vh = d.clientHeight\n      const cw = box.offsetWidth\n      const ch = box.offsetHeight\n      const inset = parseFloat(getComputedStyle(box).fontSize) * 1.4\n      let x = (vw - cw) / 2\n      let y = (vh - ch) / 2\n      if (el && line && dot) {\n        const r = el.getBoundingClientRect()\n        const down = vh - r.bottom >= ch + gap + margin || vh - r.bottom >= r.top\n        const px = clamp(r.left + r.width / 2, margin, vw - margin)\n        const py = down ? r.bottom : r.top\n        const right = px < vw / 2\n        const ax = right ? px + gap * 1.5 : px - gap * 1.5\n        x = clamp(right ? ax - inset : ax + inset - cw, margin, vw - margin - cw)\n        y = clamp(down ? r.bottom + gap : r.top - gap - ch, margin, vh - margin - ch)\n        const tx = clamp(ax, x + inset, x + cw - inset)\n        const ty = down ? y : y + ch\n        line.setAttribute(\"x1\", String(px))\n        line.setAttribute(\"y1\", String(py))\n        line.setAttribute(\"x2\", String(tx))\n        line.setAttribute(\"y2\", String(ty))\n        dot.setAttribute(\"cx\", String(px))\n        dot.setAttribute(\"cy\", String(py))\n        box.style.setProperty(\"--ot-tour-tip\", `${tx - x}px ${down ? 0 : ch}px`)\n      }\n      box.style.left = `${Math.round(x)}px`\n      box.style.top = `${Math.round(y)}px`\n      box.setAttribute(\"data-laid\", \"\")\n    }\n    if (el) {\n      const r = el.getBoundingClientRect()\n      if (r.top < margin || r.bottom > innerHeight - margin) el.scrollIntoView({ block: \"center\", inline: \"nearest\", behavior: still ? \"auto\" : \"smooth\" })\n    }\n    lay()\n    let frame = 0\n    const soon = () => {\n      cancelAnimationFrame(frame)\n      frame = requestAnimationFrame(lay)\n    }\n    addEventListener(\"scroll\", soon, { capture: true, passive: true })\n    addEventListener(\"resize\", soon)\n    const sized = new ResizeObserver(soon)\n    sized.observe(box)\n    return () => {\n      cancelAnimationFrame(frame)\n      removeEventListener(\"scroll\", soon, { capture: true })\n      removeEventListener(\"resize\", soon)\n      sized.disconnect()\n    }\n  }, [open, at, target])\n\n  // Each step is a new callout: focus goes back to the control that moved it (Next, or Back while there is one).\n  React.useEffect(() => {\n    const way = moved.current\n    moved.current = null\n    if (!way || !callout.current) return\n    const to = callout.current.querySelector<HTMLElement>(`[data-go=\"${way}\"]`) ?? callout.current.querySelector<HTMLElement>('[data-go=\"next\"]')\n    to?.focus()\n  }, [at])\n\n  const described = `${countId} ${titleId} ${descriptionId}`\n  return (\n    <DialogSurface\n      data-slot=\"tour-content\"\n      className={cn(\"ot-tour\", className)}\n      onKeyDown={(e) => {\n        onKeyDown?.(e)\n        if (e.defaultPrevented || (e.key !== \"ArrowRight\" && e.key !== \"ArrowLeft\")) return\n        e.preventDefault()\n        const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\"\n        const on = (e.key === \"ArrowRight\") !== rtl\n        if (on ? at < n : at > 1) move(on ? at + 1 : at - 1)\n      }}\n      {...props}\n    >\n      <svg key={`lead-${at}`} ref={lead} className=\"ot-tour-lead\" aria-hidden=\"true\" data-alone={step?.target ? undefined : \"\"}>\n        <line className=\"ot-tour-line\" pathLength={1} />\n        <circle className=\"ot-tour-dot\" r={3.5} />\n      </svg>\n      <div key={`callout-${at}`} ref={callout} data-slot=\"tour-callout\" className=\"ot-tour-callout\">\n        <div className=\"ot-tour-head\">\n          <h2 id={titleId} className=\"ot-tour-title\">{step?.title}</h2>\n          <span id={countId} className=\"ot-tour-count\">\n            <span aria-hidden=\"true\">\n              {two(at)}/{two(n)}\n            </span>\n            <span className=\"ot-sr\">Step {at} of {n}</span>\n          </span>\n        </div>\n        <p id={descriptionId} className=\"ot-tour-text\">{step?.children}</p>\n        <div className=\"ot-tour-actions\">\n          <button type=\"button\" className=\"ot-tour-end\" onClick={() => setOpen(false)}>\n            {end}\n          </button>\n          {at > 1 && (\n            <button type=\"button\" data-go=\"back\" aria-describedby={described} onClick={() => move(at - 1)}>\n              {back}\n            </button>\n          )}\n          <button type=\"button\" data-go=\"next\" data-last={at === n ? \"\" : undefined} data-autofocus aria-describedby={described} onClick={() => move(at + 1)}>\n            {at === n ? done : next}\n          </button>\n        </div>\n      </div>\n    </DialogSurface>\n  )\n}\n\nexport { Tour, TourTrigger, TourContent, TourStep, type TourProps, type TourStepProps, type TourContentProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/tour.css",
      "content": "@layer components {\n  /* ── ot-tour ── a tour of callouts, after Weingart's letter: an ink callout hung on a long leader line that ends\n     in a dot on the thing it is about. One step at a time, on Next or Back; it never plays by itself.\n     The dialog is the whole window and see-through: the page stays in view, and only the callout takes a hand.\n     Each step arrives as the note does: the dot lands, the leader draws out, the callout spreads from its tip. */\n  .ot-tour {\n    position: fixed; inset: 0; margin: 0; padding: 0; border: 0; width: 100%; height: 100%; max-width: none; max-height: none;\n    overflow: hidden; background: none; color: var(--ot-paper);\n    opacity: 0; transition: opacity var(--ot-moderato) var(--ot-exhale), overlay var(--ot-moderato) allow-discrete, display var(--ot-moderato) allow-discrete;\n  }\n  .ot-tour[open] { opacity: 1; }\n  @starting-style { .ot-tour[open] { opacity: 0; } }\n  .ot-tour::backdrop { background: none; }\n  .ot-tour > .ot-scrollbar { display: none; }\n\n  .ot-tour-lead { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }\n  .ot-tour-lead[data-alone] { display: none; }\n  .ot-tour-line { stroke: var(--ot-ink); stroke-width: var(--ot-hairline); stroke-dasharray: 1; animation: ot-tour-draw var(--ot-moderato) var(--ot-breath) calc(2 * var(--ot-arpeggio)) backwards; }\n  .ot-tour-dot { fill: var(--ot-accent); transform-box: fill-box; transform-origin: center; animation: ot-tour-land var(--ot-moderato) var(--ot-spiccato) backwards; }\n  @keyframes ot-tour-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }\n  @keyframes ot-tour-land { from { scale: 0; } }\n\n  /* The callout: Weingart's ink pill, small bold head, the count at the far end of it as a corner note. */\n  .ot-tour-callout {\n    position: absolute; left: 0; top: 0; box-sizing: border-box; inline-size: max-content; max-inline-size: min(36ch, calc(100vw - 2.5rem));\n    padding: 0.85em 1.15em 0.8em; border-radius: 1em; background: var(--ot-ink); color: var(--ot-paper);\n    font-family: var(--ot-voice); font-style: normal; font-weight: 450; font-size: var(--ot-pp); line-height: var(--ot-pp-lh); letter-spacing: var(--ot-pp-tr);\n    visibility: hidden;\n  }\n  .ot-tour-callout[data-laid] { visibility: visible; animation: ot-tour-spread var(--ot-andante) var(--ot-exhale) calc(5 * var(--ot-arpeggio)) backwards; }\n  @keyframes ot-tour-spread { from { clip-path: circle(0 at var(--ot-tour-tip, 1.4em 0)); } to { clip-path: circle(150% at var(--ot-tour-tip, 1.4em 0)); } }\n  .ot-tour-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1.4em; }\n  .ot-tour-title { margin: 0; font: inherit; font-weight: 700; color: var(--ot-paper); text-wrap: balance; }\n  .ot-tour-count { flex: none; font-variant-numeric: tabular-nums; color: color-mix(in srgb, var(--ot-paper) 62%, var(--ot-ink)); }\n  .ot-tour-text { margin: 0.2em 0 0; color: color-mix(in srgb, var(--ot-paper) 84%, var(--ot-ink)); text-wrap: pretty; }\n  .ot-tour-actions { display: flex; align-items: baseline; gap: 1.2em; margin-block-start: 0.9em; }\n  .ot-tour-actions > button {\n    appearance: none; border: 0; margin: 0; padding: 0; background: none; cursor: pointer; font: inherit; font-weight: 700; color: var(--ot-paper);\n    text-decoration: underline transparent var(--ot-hairline); text-underline-offset: 0.28em; transition: text-decoration-color var(--ot-allegro);\n  }\n  .ot-tour-actions > button:is(:hover, [data-force~=\"hover\"]) { text-decoration-color: currentColor; }\n  .ot-tour-actions > .ot-tour-end { margin-inline-end: auto; font-weight: 450; color: color-mix(in srgb, var(--ot-paper) 62%, var(--ot-ink)); }\n  .ot-tour-actions > [data-go=\"next\"]:not([data-last])::after { content: \"\\2009\\2192\" / \"\"; }\n  :is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-tour-actions > [data-go=\"next\"]:not([data-last])::after { content: \"\\2009\\2190\" / \"\"; }\n  .ot-tour-actions > [data-go=\"next\"][data-last]::after { content: \".\" / \"\"; }\n  /* On the ink the accent would sink: focus is a stroke under the word in the paper's colour, as a link's is. */\n  .ot-tour-actions > button:is(:focus-visible, [data-force~=\"focus\"]) { outline: none; text-decoration: underline var(--ot-stroke) var(--ot-paper); }\n  @media (forced-colors: active) { .ot-tour-actions > button:is(:focus-visible, [data-force~=\"focus\"]) { outline: var(--ot-stroke) solid CanvasText; } }\n\n  @media (forced-colors: active) {\n    .ot-tour-callout { border: var(--ot-hairline) solid CanvasText; }\n    .ot-tour-line { stroke: CanvasText; }\n    .ot-tour-dot { fill: Highlight; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/tour.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/tour.css\"": {}
  },
  "meta": {
    "movement": "VIII",
    "underneath": "native",
    "contract": "ot-tour"
  },
  "type": "registry:ui"
}
