{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tour",
  "title": "Tour",
  "description": "A step-by-step product tour that spotlights real elements on the page.",
  "dependencies": [
    "radix-ui",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "popover",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/tour.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { XIcon } from \"lucide-react\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/registry/default/lib/utils\"\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport {\n  Popover,\n  PopoverAnchor,\n  PopoverContent,\n} from \"@/registry/default/ui/popover\"\nimport {\n  TourSpotlight,\n  findTourTarget,\n  useScrollTargetIntoView,\n} from \"@/registry/default/ui/tour-spotlight\"\n\ntype TourStep = {\n  target: string\n  title: string\n  description?: React.ReactNode\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n}\n\nfunction TourTarget({\n  name,\n  ...props\n}: React.ComponentProps<typeof Slot.Root> & { name: string }) {\n  return <Slot.Root data-tour-target={name} {...props} />\n}\n\nfunction TourProgress({ count, current }: { count: number; current: number }) {\n  return (\n    <div aria-hidden className=\"flex items-center gap-1\">\n      {Array.from({ length: count }, (_, index) => (\n        <span\n          key={index}\n          className={cn(\n            \"h-1.5 rounded-full bg-muted-foreground/30 transition-all duration-300\",\n            index === current ? \"w-4 bg-foreground\" : \"w-1.5\"\n          )}\n        />\n      ))}\n    </div>\n  )\n}\n\nfunction Tour({\n  steps,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  step,\n  defaultStep = 0,\n  onStepChange,\n  onComplete,\n  children,\n}: {\n  steps: TourStep[]\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  step?: number\n  defaultStep?: number\n  onStepChange?: (step: number) => void\n  onComplete?: () => void\n  children?: React.ReactNode\n}) {\n  const titleId = React.useId()\n  const nextButtonRef = React.useRef<HTMLButtonElement>(null)\n  const returnFocusRef = React.useRef<HTMLElement | null>(null)\n  const { value: isOpen, setValue: setIsOpen } = useControllableState({\n    value: open,\n    defaultValue: defaultOpen,\n    onChange: onOpenChange,\n  })\n  const { value: stepIndex, setValue: setStepIndex } = useControllableState({\n    value: step,\n    defaultValue: defaultStep,\n    onChange: onStepChange,\n  })\n  const currentStep = isOpen ? steps[stepIndex] : undefined\n  const isLastStep = stepIndex === steps.length - 1\n  const targetName = currentStep?.target ?? null\n  useScrollTargetIntoView(targetName)\n\n  const virtualAnchor = React.useMemo(\n    () => ({\n      current: {\n        getBoundingClientRect: () =>\n          (targetName && findTourTarget(targetName)?.getBoundingClientRect()) ||\n          new DOMRect(),\n      },\n    }),\n    [targetName]\n  )\n\n  function endTour() {\n    setIsOpen(false)\n    setStepIndex(0)\n  }\n\n  function goNext() {\n    if (!isLastStep) return setStepIndex(stepIndex + 1)\n    endTour()\n    onComplete?.()\n  }\n\n  function goBack() {\n    if (stepIndex > 0) setStepIndex(stepIndex - 1)\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n    switch (event.key) {\n      case \"ArrowRight\":\n        return goNext()\n      case \"ArrowLeft\":\n        return goBack()\n    }\n  }\n\n  return (\n    <>\n      {children}\n      {currentStep && (\n        <Popover open modal onOpenChange={(next) => !next && endTour()}>\n          <PopoverAnchor virtualRef={virtualAnchor} />\n          <TourSpotlight target={currentStep.target} />\n          <PopoverContent\n            data-slot=\"tour-card\"\n            role=\"dialog\"\n            aria-labelledby={titleId}\n            side={currentStep.side ?? \"bottom\"}\n            sideOffset={14}\n            updatePositionStrategy=\"always\"\n            onOpenAutoFocus={(event) => {\n              event.preventDefault()\n              if (document.activeElement instanceof HTMLElement)\n                returnFocusRef.current = document.activeElement\n              nextButtonRef.current?.focus()\n            }}\n            onCloseAutoFocus={(event) => {\n              event.preventDefault()\n              returnFocusRef.current?.focus()\n              returnFocusRef.current = null\n            }}\n            onInteractOutside={(event) => event.preventDefault()}\n            onKeyDown={handleKeyDown}\n            className=\"flex w-80 flex-col gap-3\"\n          >\n            <div className=\"flex items-start justify-between gap-4\">\n              <div className=\"flex flex-col gap-1\">\n                <span className=\"font-mono text-[11px] text-muted-foreground\">\n                  {stepIndex + 1} of {steps.length}\n                </span>\n                <h2 id={titleId} className=\"leading-none font-medium\">\n                  {currentStep.title}\n                </h2>\n              </div>\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                aria-label=\"End tour\"\n                onClick={endTour}\n                className=\"-mt-1 -mr-2 size-7\"\n              >\n                <XIcon />\n              </Button>\n            </div>\n            {currentStep.description && (\n              <div className=\"text-sm text-muted-foreground\">\n                {currentStep.description}\n              </div>\n            )}\n            <div className=\"flex items-center justify-between\">\n              <TourProgress count={steps.length} current={stepIndex} />\n              <div className=\"flex gap-2\">\n                {stepIndex > 0 && (\n                  <Button variant=\"ghost\" size=\"sm\" onClick={goBack}>\n                    Back\n                  </Button>\n                )}\n                <Button ref={nextButtonRef} size=\"sm\" onClick={goNext}>\n                  {isLastStep ? \"Finish\" : \"Next\"}\n                </Button>\n              </div>\n            </div>\n          </PopoverContent>\n        </Popover>\n      )}\n    </>\n  )\n}\n\nexport { Tour, TourTarget, type TourStep }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/ui/tour-spotlight.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { createPortal } from \"react-dom\"\n\nconst SPOTLIGHT_PADDING_PX = 6\n\nfunction subscribeToNothing() {\n  return () => {}\n}\n\nfunction findTourTarget(name: string) {\n  return document.querySelector<HTMLElement>(\n    `[data-tour-target=\"${CSS.escape(name)}\"]`\n  )\n}\n\nfunction useScrollTargetIntoView(target: string | null) {\n  React.useEffect(() => {\n    if (!target) return\n    const prefersReducedMotion = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches\n    findTourTarget(target)?.scrollIntoView({\n      block: \"center\",\n      behavior: prefersReducedMotion ? \"auto\" : \"smooth\",\n    })\n  }, [target])\n}\n\nfunction useFollowTarget(\n  target: string,\n  spotlightRef: React.RefObject<HTMLDivElement | null>\n) {\n  React.useEffect(() => {\n    let frameId = 0\n    function follow() {\n      const element = findTourTarget(target)\n      const spotlight = spotlightRef.current\n      if (element && spotlight) {\n        const rect = element.getBoundingClientRect()\n        spotlight.style.top = `${rect.top - SPOTLIGHT_PADDING_PX}px`\n        spotlight.style.left = `${rect.left - SPOTLIGHT_PADDING_PX}px`\n        spotlight.style.width = `${rect.width + SPOTLIGHT_PADDING_PX * 2}px`\n        spotlight.style.height = `${rect.height + SPOTLIGHT_PADDING_PX * 2}px`\n        spotlight.style.opacity = \"1\"\n      }\n      frameId = requestAnimationFrame(follow)\n    }\n    follow()\n    return () => cancelAnimationFrame(frameId)\n  }, [target, spotlightRef])\n}\n\nfunction TourSpotlight({ target }: { target: string }) {\n  const spotlightRef = React.useRef<HTMLDivElement>(null)\n  const isClient = React.useSyncExternalStore(\n    subscribeToNothing,\n    () => true,\n    () => false\n  )\n  useFollowTarget(target, spotlightRef)\n\n  if (!isClient) return null\n\n  return createPortal(\n    <>\n      <div\n        aria-hidden\n        data-slot=\"tour-overlay\"\n        className=\"fixed inset-0 z-50\"\n      />\n      <div\n        ref={spotlightRef}\n        aria-hidden\n        data-slot=\"tour-spotlight\"\n        className=\"pointer-events-none fixed z-50 rounded-lg opacity-0 shadow-[0_0_0_9999px_rgb(0_0_0/0.55)] ring-2 ring-ring transition-opacity duration-200\"\n      />\n    </>,\n    document.body\n  )\n}\n\nexport { TourSpotlight, findTourTarget, useScrollTargetIntoView }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}