{
  "$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": [
    "@base-ui/react",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "popover",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/base/ui/tour.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\"\nimport { useRender } from \"@base-ui/react/use-render\"\nimport { XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/registry/default/lib/utils\"\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport { Button } from \"@/registry/base/ui/button\"\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  children,\n  ...props\n}: Omit<React.HTMLAttributes<HTMLElement>, \"children\"> & {\n  name: string\n  children: React.ReactElement\n}) {\n  return useRender({\n    render: children,\n    props: { \"data-tour-target\": name, ...props },\n  })\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 { 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 findAnchor = React.useCallback(\n    () => (targetName ? findTourTarget(targetName) : null),\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        <PopoverPrimitive.Root\n          open\n          modal\n          onOpenChange={(next, { reason }) => {\n            if (!next && reason !== \"outside-press\") endTour()\n          }}\n        >\n          <TourSpotlight target={currentStep.target} />\n          <PopoverPrimitive.Portal>\n            <PopoverPrimitive.Positioner\n              anchor={findAnchor}\n              side={currentStep.side ?? \"bottom\"}\n              sideOffset={14}\n              className=\"isolate z-50\"\n            >\n              <PopoverPrimitive.Popup\n                data-slot=\"tour-card\"\n                aria-labelledby={titleId}\n                initialFocus={nextButtonRef}\n                onKeyDown={handleKeyDown}\n                className=\"z-50 flex w-80 origin-(--transform-origin) flex-col gap-3 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95\"\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                  <PopoverPrimitive.Close\n                    render={\n                      <Button\n                        variant=\"ghost\"\n                        size=\"icon\"\n                        aria-label=\"End tour\"\n                        className=\"-mt-1 -mr-2 size-7\"\n                      />\n                    }\n                  >\n                    <XIcon />\n                  </PopoverPrimitive.Close>\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              </PopoverPrimitive.Popup>\n            </PopoverPrimitive.Positioner>\n          </PopoverPrimitive.Portal>\n        </PopoverPrimitive.Root>\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"
}