{
  "name": "tour",
  "type": "registry:ui",
  "dependencies": [
    "cn",
    "@floating-ui/react-dom",
    "radix-ui"
  ],
  "registryDependencies": [
    "@diceui/use-as-ref",
    "@diceui/use-isomorphic-layout-effect",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/tour.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  autoUpdate,\n  flip,\n  hide,\n  limitShift,\n  type Middleware,\n  offset,\n  arrow as onArrow,\n  type Placement,\n  shift,\n  useFloating,\n} from \"@floating-ui/react-dom\";\nimport { cn } from \"cn\";\nimport {\n  Direction as DirectionPrimitive,\n  Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\nimport { Button } from \"@/registry/bases/radix/ui/button\";\nimport { IconPlaceholder } from \"@/registry/icons/icon-placeholder\";\n\nconst ROOT_NAME = \"Tour\";\nconst PORTAL_NAME = \"TourPortal\";\nconst STEP_NAME = \"TourStep\";\nconst ARROW_NAME = \"TourArrow\";\nconst HEADER_NAME = \"TourHeader\";\nconst TITLE_NAME = \"TourTitle\";\nconst DESCRIPTION_NAME = \"TourDescription\";\nconst CLOSE_NAME = \"TourClose\";\nconst PREV_NAME = \"TourPrev\";\nconst NEXT_NAME = \"TourNext\";\nconst SKIP_NAME = \"TourSkip\";\nconst FOOTER_NAME = \"TourFooter\";\n\nconst POINTER_DOWN_OUTSIDE = \"tour.pointerDownOutside\";\nconst INTERACT_OUTSIDE = \"tour.interactOutside\";\nconst OPEN_AUTO_FOCUS = \"tour.openAutoFocus\";\nconst CLOSE_AUTO_FOCUS = \"tour.closeAutoFocus\";\nconst EVENT_OPTIONS = { bubbles: false, cancelable: true };\n\nconst SIDE_OPTIONS = [\"top\", \"right\", \"bottom\", \"left\"] as const;\nconst ALIGN_OPTIONS = [\"start\", \"center\", \"end\"] as const;\n\nconst DEFAULT_ALIGN_OFFSET = 0;\nconst DEFAULT_SIDE_OFFSET = 16;\nconst DEFAULT_SPOTLIGHT_PADDING = 4;\n\ntype Side = (typeof SIDE_OPTIONS)[number];\ntype Align = (typeof ALIGN_OPTIONS)[number];\ntype Direction = \"ltr\" | \"rtl\";\n\ninterface ScrollOffset {\n  top?: number;\n  bottom?: number;\n  left?: number;\n  right?: number;\n}\n\ntype Boundary = Element | null;\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ntype StepElement = React.ComponentRef<typeof TourStep>;\ntype CloseElement = React.ComponentRef<typeof TourClose>;\ntype PrevElement = React.ComponentRef<typeof TourPrev>;\ntype NextElement = React.ComponentRef<typeof TourNext>;\ntype SkipElement = React.ComponentRef<typeof TourSkip>;\ntype FooterElement = React.ComponentRef<typeof TourFooter>;\n\nconst OPPOSITE_SIDE: Record<Side, Side> = {\n  top: \"bottom\",\n  right: \"left\",\n  bottom: \"top\",\n  left: \"right\",\n};\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/focus-guards/src/focus-guards.tsx\n */\nlet focusGuardCount = 0;\n\nfunction createFocusGuard() {\n  const element = document.createElement(\"span\");\n  element.setAttribute(\"data-tour-focus-guard\", \"\");\n  element.tabIndex = 0;\n  element.style.outline = \"none\";\n  element.style.opacity = \"0\";\n  element.style.position = \"fixed\";\n  element.style.pointerEvents = \"none\";\n  return element;\n}\n\nfunction useFocusGuards() {\n  React.useEffect(() => {\n    const edgeGuards = document.querySelectorAll(\"[data-tour-focus-guard]\");\n    document.body.insertAdjacentElement(\n      \"afterbegin\",\n      edgeGuards[0] ?? createFocusGuard(),\n    );\n    document.body.insertAdjacentElement(\n      \"beforeend\",\n      edgeGuards[1] ?? createFocusGuard(),\n    );\n    focusGuardCount++;\n\n    return () => {\n      if (focusGuardCount === 1) {\n        const guards = document.querySelectorAll(\"[data-tour-focus-guard]\");\n        for (const node of guards) {\n          node.remove();\n        }\n      }\n      focusGuardCount--;\n    };\n  }, []);\n}\n\nfunction useFocusTrap(\n  containerRef: React.RefObject<HTMLElement | null>,\n  enabled: boolean,\n  tourOpen: boolean,\n  onOpenAutoFocus?: (event: OpenAutoFocusEvent) => void,\n  onCloseAutoFocus?: (event: CloseAutoFocusEvent) => void,\n) {\n  const lastFocusedElementRef = React.useRef<HTMLElement | null>(null);\n  const onOpenAutoFocusRef = useAsRef(onOpenAutoFocus);\n  const onCloseAutoFocusRef = useAsRef(onCloseAutoFocus);\n  const tourOpenRef = useAsRef(tourOpen);\n\n  React.useEffect(() => {\n    if (!enabled) return;\n\n    const container = containerRef.current;\n    if (!container) return;\n\n    const previouslyFocusedElement =\n      document.activeElement as HTMLElement | null;\n\n    function getTabbableCandidates() {\n      if (!container) return [];\n\n      const nodes: HTMLElement[] = [];\n      const walker = document.createTreeWalker(\n        container,\n        NodeFilter.SHOW_ELEMENT,\n        {\n          acceptNode: (node: Element) => {\n            const element = node as HTMLElement;\n            const isHiddenInput =\n              element.tagName === \"INPUT\" &&\n              (element as HTMLInputElement).type === \"hidden\";\n            if (element.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;\n            return element.tabIndex >= 0\n              ? NodeFilter.FILTER_ACCEPT\n              : NodeFilter.FILTER_SKIP;\n          },\n        },\n      );\n      while (walker.nextNode()) {\n        nodes.push(walker.currentNode as HTMLElement);\n      }\n      return nodes;\n    }\n\n    function getTabbableEdges() {\n      const candidates = getTabbableCandidates();\n      const first = candidates[0];\n      const last = candidates[candidates.length - 1];\n      return [first, last] as const;\n    }\n\n    function onFocusIn(event: FocusEvent) {\n      if (!container) return;\n\n      const target = event.target as HTMLElement | null;\n      if (container.contains(target)) {\n        lastFocusedElementRef.current = target;\n      } else {\n        const elementToFocus =\n          lastFocusedElementRef.current ?? getTabbableCandidates()[0];\n        elementToFocus?.focus({ preventScroll: true });\n      }\n    }\n\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key !== \"Tab\" || event.altKey || event.ctrlKey || event.metaKey)\n        return;\n\n      const [first, last] = getTabbableEdges();\n      const hasTabbableElements = first && last;\n\n      if (!hasTabbableElements) {\n        if (document.activeElement === container) event.preventDefault();\n        return;\n      }\n\n      if (!event.shiftKey && document.activeElement === last) {\n        event.preventDefault();\n        first?.focus({ preventScroll: true });\n      } else if (event.shiftKey && document.activeElement === first) {\n        event.preventDefault();\n        last?.focus({ preventScroll: true });\n      }\n    }\n\n    const openAutoFocusEvent = new CustomEvent(OPEN_AUTO_FOCUS, EVENT_OPTIONS);\n    if (onOpenAutoFocusRef.current) {\n      container.addEventListener(\n        OPEN_AUTO_FOCUS,\n        onOpenAutoFocusRef.current as EventListener,\n        { once: true },\n      );\n    }\n    container.dispatchEvent(openAutoFocusEvent);\n\n    if (!openAutoFocusEvent.defaultPrevented) {\n      const tabbableCandidates = getTabbableCandidates();\n      if (tabbableCandidates.length > 0) {\n        tabbableCandidates[0]?.focus({ preventScroll: true });\n      } else {\n        container.focus({ preventScroll: true });\n      }\n    }\n\n    document.addEventListener(\"focusin\", onFocusIn);\n    container.addEventListener(\"keydown\", onKeyDown);\n\n    return () => {\n      document.removeEventListener(\"focusin\", onFocusIn);\n      container.removeEventListener(\"keydown\", onKeyDown);\n\n      if (!tourOpenRef.current) {\n        setTimeout(() => {\n          const closeAutoFocusEvent = new CustomEvent(\n            CLOSE_AUTO_FOCUS,\n            EVENT_OPTIONS,\n          );\n          if (onCloseAutoFocusRef.current) {\n            container.addEventListener(\n              CLOSE_AUTO_FOCUS,\n              onCloseAutoFocusRef.current as EventListener,\n              { once: true },\n            );\n          }\n          container.dispatchEvent(closeAutoFocusEvent);\n\n          if (!closeAutoFocusEvent.defaultPrevented) {\n            if (\n              previouslyFocusedElement &&\n              document.body.contains(previouslyFocusedElement)\n            ) {\n              previouslyFocusedElement.focus({ preventScroll: true });\n            }\n          }\n\n          if (onCloseAutoFocusRef.current) {\n            container.removeEventListener(\n              CLOSE_AUTO_FOCUS,\n              onCloseAutoFocusRef.current as EventListener,\n            );\n          }\n        }, 0);\n      }\n    };\n  }, [\n    containerRef,\n    enabled,\n    onOpenAutoFocusRef,\n    onCloseAutoFocusRef,\n    tourOpenRef,\n  ]);\n}\n\nfunction getDataState(open: boolean) {\n  return open ? \"open\" : \"closed\";\n}\n\ninterface StepData {\n  target: string | React.RefObject<HTMLElement> | HTMLElement;\n  align?: Align;\n  alignOffset?: number;\n  side?: Side;\n  sideOffset?: number;\n  collisionBoundary?: Boundary | Boundary[];\n  collisionPadding?: number | Partial<Record<Side, number>>;\n  arrowPadding?: number;\n  sticky?: \"partial\" | \"always\";\n  hideWhenDetached?: boolean;\n  avoidCollisions?: boolean;\n  onStepEnter?: () => void;\n  onStepLeave?: () => void;\n  required?: boolean;\n}\n\ninterface StoreState {\n  open: boolean;\n  value: number;\n  steps: StepData[];\n  maskPath: string;\n  spotlightRect: { x: number; y: number; width: number; height: number } | null;\n}\n\ninterface Store {\n  subscribe: (callback: () => void) => () => void;\n  getState: () => StoreState;\n  setState: <K extends keyof StoreState>(\n    key: K,\n    value: StoreState[K],\n    opts?: unknown,\n  ) => void;\n  notify: () => void;\n  addStep: (stepData: StepData) => { id: string; index: number };\n  removeStep: (id: string) => void;\n}\n\nfunction useStore<T>(\n  selector: (state: StoreState) => T,\n  ogStore?: Store | null,\n): T {\n  const contextStore = React.useContext(StoreContext);\n\n  const store = ogStore ?? contextStore;\n\n  if (!store) {\n    throw new Error(`\\`useStore\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n\n  const getSnapshot = React.useCallback(\n    () => selector(store.getState()),\n    [store, selector],\n  );\n\n  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\nfunction getTargetElement(\n  target: string | React.RefObject<HTMLElement> | HTMLElement,\n): HTMLElement | null {\n  if (typeof target === \"string\") {\n    return document.querySelector(target);\n  }\n  if (target && \"current\" in target) {\n    return target.current;\n  }\n  if (target instanceof HTMLElement) {\n    return target;\n  }\n  return null;\n}\n\nfunction getDefaultScrollBehavior(): ScrollBehavior {\n  if (typeof window === \"undefined\") return \"smooth\";\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    ? \"auto\"\n    : \"smooth\";\n}\n\nfunction onScrollToElement(\n  element: HTMLElement,\n  scrollBehavior: ScrollBehavior = getDefaultScrollBehavior(),\n  scrollOffset?: ScrollOffset,\n) {\n  const offset: Required<ScrollOffset> = {\n    top: 100,\n    bottom: 100,\n    left: 0,\n    right: 0,\n    ...scrollOffset,\n  };\n  const rect = element.getBoundingClientRect();\n  const viewportHeight = window.innerHeight;\n  const viewportWidth = window.innerWidth;\n\n  const isInViewport =\n    rect.top >= offset.top &&\n    rect.bottom <= viewportHeight - offset.bottom &&\n    rect.left >= offset.left &&\n    rect.right <= viewportWidth - offset.right;\n\n  if (!isInViewport) {\n    const elementTop = rect.top + window.scrollY;\n    const scrollTop = elementTop - offset.top;\n\n    window.scrollTo({\n      top: Math.max(0, scrollTop),\n      behavior: scrollBehavior,\n    });\n  }\n}\n\nfunction getSideAndAlignFromPlacement(placement: Placement): [Side, Align] {\n  const [side, align = \"center\"] = placement.split(\"-\") as [Side, Align?];\n  return [side, align];\n}\n\nfunction getPlacement(side: Side, align: Align): Placement {\n  if (align === \"center\") {\n    return side as Placement;\n  }\n  return `${side}-${align}` as Placement;\n}\n\nfunction updateMask(\n  store: Store,\n  targetElement: HTMLElement,\n  padding: number = DEFAULT_SPOTLIGHT_PADDING,\n) {\n  const clientRect = targetElement.getBoundingClientRect();\n  const viewportWidth = window.innerWidth;\n  const viewportHeight = window.innerHeight;\n\n  const x = Math.max(0, clientRect.left - padding);\n  const y = Math.max(0, clientRect.top - padding);\n  const width = Math.min(viewportWidth - x, clientRect.width + padding * 2);\n  const height = Math.min(viewportHeight - y, clientRect.height + padding * 2);\n\n  const path = `polygon(0% 0%, 0% 100%, ${x}px 100%, ${x}px ${y}px, ${x + width}px ${y}px, ${x + width}px ${y + height}px, ${x}px ${y + height}px, ${x}px 100%, 100% 100%, 100% 0%)`;\n  store.setState(\"maskPath\", path);\n  store.setState(\"spotlightRect\", { x, y, width, height });\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n  const context = React.useContext(StoreContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface TourContextValue {\n  dir: Direction;\n  alignOffset: number;\n  sideOffset: number;\n  spotlightPadding: number;\n  dismissible: boolean;\n  modal: boolean;\n  stepFooter?: React.ReactElement;\n  onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;\n  onInteractOutside?: (event: InteractOutsideEvent) => void;\n  onOpenAutoFocus?: (event: OpenAutoFocusEvent) => void;\n  onCloseAutoFocus?: (event: CloseAutoFocusEvent) => void;\n}\n\nconst TourContext = React.createContext<TourContextValue | null>(null);\n\nfunction useTourContext(consumerName: string) {\n  const context = React.useContext(TourContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface StepContextValue {\n  arrowX?: number;\n  arrowY?: number;\n  placedAlign: Align;\n  placedSide: Side;\n  shouldHideArrow: boolean;\n  onArrowChange: (arrow: HTMLSpanElement | null) => void;\n  onFooterChange: (footer: FooterElement | null) => void;\n}\n\nconst StepContext = React.createContext<StepContextValue | null>(null);\n\nfunction useStepContext(consumerName: string) {\n  const context = React.useContext(StepContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${STEP_NAME}\\``);\n  }\n  return context;\n}\n\nconst DefaultFooterContext = React.createContext(false);\n\ninterface PortalContextValue {\n  portal: HTMLElement | null;\n  onPortalChange: (node: HTMLElement | null) => void;\n}\n\nconst PortalContext = React.createContext<PortalContextValue | null>(null);\n\nfunction usePortalContext(consumerName: string) {\n  const context = React.useContext(PortalContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\nfunction useScrollLock(enabled: boolean) {\n  React.useEffect(() => {\n    if (!enabled) return;\n\n    const originalStyle = window.getComputedStyle(document.body).overflow;\n    const scrollbarWidth =\n      window.innerWidth - document.documentElement.clientWidth;\n\n    document.body.style.overflow = \"hidden\";\n    if (scrollbarWidth > 0) {\n      document.body.style.paddingRight = `${scrollbarWidth}px`;\n    }\n\n    return () => {\n      document.body.style.overflow = originalStyle;\n      document.body.style.paddingRight = \"\";\n    };\n  }, [enabled]);\n}\n\ntype PointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }>;\ntype InteractOutsideEvent = CustomEvent<{\n  originalEvent: PointerEvent | FocusEvent;\n}>;\ntype OpenAutoFocusEvent = CustomEvent<Record<string, never>>;\ntype CloseAutoFocusEvent = CustomEvent<Record<string, never>>;\n\ninterface TourProps extends DivProps {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (step: number) => void;\n  onComplete?: () => void;\n  onSkip?: () => void;\n  onEscapeKeyDown?: (event: KeyboardEvent) => void;\n  onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;\n  onInteractOutside?: (event: InteractOutsideEvent) => void;\n  onOpenAutoFocus?: (event: OpenAutoFocusEvent) => void;\n  onCloseAutoFocus?: (event: CloseAutoFocusEvent) => void;\n  dir?: Direction;\n  alignOffset?: number;\n  sideOffset?: number;\n  spotlightPadding?: number;\n  autoScroll?: boolean;\n  scrollBehavior?: ScrollBehavior;\n  scrollOffset?: ScrollOffset;\n  dismissible?: boolean;\n  modal?: boolean;\n  stepFooter?: React.ReactElement;\n}\n\nfunction Tour(props: TourProps) {\n  const {\n    open: openProp,\n    defaultOpen = false,\n    onOpenChange,\n    value: valueProp,\n    defaultValue = 0,\n    onValueChange,\n    onComplete,\n    onSkip,\n    autoScroll = true,\n    scrollBehavior = getDefaultScrollBehavior(),\n    scrollOffset,\n    onEscapeKeyDown,\n    onPointerDownOutside,\n    onInteractOutside,\n    onOpenAutoFocus,\n    onCloseAutoFocus,\n    dir: dirProp,\n    alignOffset = DEFAULT_ALIGN_OFFSET,\n    sideOffset = DEFAULT_SIDE_OFFSET,\n    spotlightPadding = DEFAULT_SPOTLIGHT_PADDING,\n    dismissible = true,\n    modal = true,\n    stepFooter,\n    asChild,\n    ...rootProps\n  } = props;\n\n  const dir = DirectionPrimitive.useDirection(dirProp);\n\n  const [portal, setPortal] = React.useState<HTMLElement | null>(null);\n  const prevOpenRef = React.useRef<boolean | undefined>(undefined);\n  const previouslyFocusedElementRef = React.useRef<HTMLElement | null>(null);\n\n  const stateRef = useLazyRef<StoreState>(() => ({\n    open: openProp ?? defaultOpen,\n    value: valueProp ?? defaultValue,\n    steps: [],\n    maskPath: \"\",\n    spotlightRect: null,\n  }));\n  const listenersRef = useLazyRef<Set<() => void>>(() => new Set());\n  const stepIdsMapRef = useLazyRef<Map<string, number>>(() => new Map());\n  const stepIdCounterRef = useLazyRef(() => ({ current: 0 }));\n  const propsRef = useAsRef({\n    valueProp,\n    onOpenChange,\n    onValueChange,\n    onComplete,\n    onSkip,\n    onEscapeKeyDown,\n    onCloseAutoFocus,\n    autoScroll,\n    scrollBehavior,\n    scrollOffset,\n  });\n\n  const store: Store = React.useMemo(\n    () => ({\n      subscribe: (cb) => {\n        listenersRef.current.add(cb);\n        return () => listenersRef.current.delete(cb);\n      },\n      getState: () => {\n        return stateRef.current;\n      },\n      setState: (key, value) => {\n        if (Object.is(stateRef.current[key], value)) return;\n        stateRef.current[key] = value;\n\n        if (key === \"open\" && typeof value === \"boolean\") {\n          propsRef.current.onOpenChange?.(value);\n\n          if (value) {\n            if (stateRef.current.steps.length > 0) {\n              if (stateRef.current.value >= stateRef.current.steps.length) {\n                store.setState(\"value\", 0);\n              }\n            }\n          } else {\n            if (\n              stateRef.current.value <\n              (stateRef.current.steps.length || 0) - 1\n            ) {\n              propsRef.current.onSkip?.();\n            }\n          }\n        } else if (key === \"value\" && typeof value === \"number\") {\n          const prevStep = stateRef.current.steps[stateRef.current.value];\n          const nextStep = stateRef.current.steps[value];\n\n          prevStep?.onStepLeave?.();\n          nextStep?.onStepEnter?.();\n\n          if (value >= stateRef.current.steps.length) {\n            propsRef.current.onComplete?.();\n\n            if (propsRef.current.valueProp !== undefined) {\n              propsRef.current.onValueChange?.(value);\n            }\n\n            store.setState(\"open\", false);\n            return;\n          }\n\n          if (propsRef.current.valueProp !== undefined) {\n            propsRef.current.onValueChange?.(value);\n            return;\n          }\n\n          propsRef.current.onValueChange?.(value);\n\n          if (nextStep && propsRef.current.autoScroll) {\n            const targetElement = getTargetElement(nextStep.target);\n            if (targetElement) {\n              onScrollToElement(\n                targetElement,\n                propsRef.current.scrollBehavior,\n                propsRef.current.scrollOffset,\n              );\n            }\n          }\n        }\n\n        store.notify();\n      },\n      notify: () => {\n        listenersRef.current.forEach((l) => {\n          l();\n        });\n      },\n      addStep: (stepData) => {\n        const id = `step-${stepIdCounterRef.current.current++}`;\n        const index = stateRef.current.steps.length;\n        stepIdsMapRef.current.set(id, index);\n        stateRef.current.steps = [...stateRef.current.steps, stepData];\n        store.notify();\n        return { id, index };\n      },\n      removeStep: (id) => {\n        const index = stepIdsMapRef.current.get(id);\n        if (index === undefined) return;\n\n        stateRef.current.steps = stateRef.current.steps.filter(\n          (_, i) => i !== index,\n        );\n\n        stepIdsMapRef.current.delete(id);\n\n        for (const [stepId, stepIndex] of stepIdsMapRef.current.entries()) {\n          if (stepIndex > index) {\n            stepIdsMapRef.current.set(stepId, stepIndex - 1);\n          }\n        }\n\n        store.notify();\n      },\n    }),\n    [stateRef, listenersRef, stepIdsMapRef, stepIdCounterRef, propsRef],\n  );\n\n  const open = useStore((state) => state.open, store);\n\n  React.useEffect(() => {\n    function onKeyDown(event: KeyboardEvent) {\n      if (open && event.key === \"Escape\") {\n        if (propsRef.current.onEscapeKeyDown) {\n          propsRef.current.onEscapeKeyDown(event);\n          if (event.defaultPrevented) return;\n        }\n        store.setState(\"open\", false);\n      }\n    }\n\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => document.removeEventListener(\"keydown\", onKeyDown);\n  }, [store, open, propsRef]);\n\n  useIsomorphicLayoutEffect(() => {\n    const wasOpen = prevOpenRef.current;\n\n    if (open && !wasOpen) {\n      previouslyFocusedElementRef.current =\n        document.activeElement as HTMLElement | null;\n    } else if (!open && wasOpen) {\n      setTimeout(() => {\n        const container = portal ?? document.body;\n        const closeAutoFocusEvent = new CustomEvent(\n          CLOSE_AUTO_FOCUS,\n          EVENT_OPTIONS,\n        );\n\n        if (propsRef.current.onCloseAutoFocus) {\n          container.addEventListener(\n            CLOSE_AUTO_FOCUS,\n            propsRef.current.onCloseAutoFocus as EventListener,\n            { once: true },\n          );\n        }\n        container.dispatchEvent(closeAutoFocusEvent);\n\n        if (!closeAutoFocusEvent.defaultPrevented) {\n          const elementToFocus = previouslyFocusedElementRef.current;\n          if (elementToFocus && document.body.contains(elementToFocus)) {\n            elementToFocus.focus({ preventScroll: true });\n          }\n        }\n\n        previouslyFocusedElementRef.current = null;\n      }, 0);\n    }\n\n    prevOpenRef.current = open;\n  }, [open, portal, propsRef]);\n\n  useIsomorphicLayoutEffect(() => {\n    if (openProp !== undefined) {\n      store.setState(\"open\", openProp);\n    }\n  }, [openProp, store]);\n\n  useIsomorphicLayoutEffect(() => {\n    if (valueProp !== undefined) {\n      store.setState(\"value\", valueProp);\n    }\n  }, [valueProp, store]);\n\n  const contextValue = React.useMemo<TourContextValue>(\n    () => ({\n      dir,\n      alignOffset,\n      sideOffset,\n      spotlightPadding,\n      dismissible,\n      modal,\n      stepFooter,\n      onPointerDownOutside,\n      onInteractOutside,\n      onOpenAutoFocus,\n      onCloseAutoFocus,\n    }),\n    [\n      dir,\n      alignOffset,\n      sideOffset,\n      spotlightPadding,\n      dismissible,\n      modal,\n      stepFooter,\n      onPointerDownOutside,\n      onInteractOutside,\n      onOpenAutoFocus,\n      onCloseAutoFocus,\n    ],\n  );\n\n  const portalContextValue = React.useMemo<PortalContextValue>(\n    () => ({\n      portal,\n      onPortalChange: setPortal,\n    }),\n    [portal],\n  );\n\n  useScrollLock(open && modal);\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StoreContext.Provider value={store}>\n      <TourContext.Provider value={contextValue}>\n        <PortalContext.Provider value={portalContextValue}>\n          <RootPrimitive data-slot=\"tour\" dir={dir} {...rootProps} />\n        </PortalContext.Provider>\n      </TourContext.Provider>\n    </StoreContext.Provider>\n  );\n}\n\ninterface TourStepProps extends DivProps {\n  target: string | React.RefObject<HTMLElement> | HTMLElement;\n  side?: Side;\n  sideOffset?: number;\n  align?: Align;\n  alignOffset?: number;\n  collisionBoundary?: Boundary | Boundary[];\n  collisionPadding?: number | Partial<Record<Side, number>>;\n  arrowPadding?: number;\n  sticky?: \"partial\" | \"always\";\n  hideWhenDetached?: boolean;\n  avoidCollisions?: boolean;\n  required?: boolean;\n  forceMount?: boolean;\n  onStepEnter?: () => void;\n  onStepLeave?: () => void;\n}\n\nfunction TourStep(props: TourStepProps) {\n  const {\n    target,\n    side = \"bottom\",\n    sideOffset,\n    align = \"center\",\n    alignOffset,\n    collisionBoundary = [],\n    collisionPadding = 0,\n    arrowPadding = 0,\n    sticky = \"partial\",\n    hideWhenDetached = false,\n    avoidCollisions = true,\n    required = false,\n    forceMount = false,\n    onStepEnter,\n    onStepLeave,\n    onPointerDownCapture: onPointerDownCaptureProp,\n    onFocusCapture: onFocusCaptureProp,\n    onBlurCapture: onBlurCaptureProp,\n    children,\n    className,\n    style,\n    asChild,\n    ...stepProps\n  } = props;\n\n  const store = useStoreContext(STEP_NAME);\n\n  const [arrow, setArrow] = React.useState<HTMLSpanElement | null>(null);\n  const [footer, setFooter] = React.useState<FooterElement | null>(null);\n\n  const stepRef = React.useRef<StepElement | null>(null);\n  const stepIdRef = React.useRef<string>(\"\");\n  const stepOrderRef = React.useRef<number>(-1);\n  const isPointerInsideReactTreeRef = React.useRef(false);\n  const isFocusInsideReactTreeRef = React.useRef(false);\n\n  const open = useStore((state) => state.open);\n  const value = useStore((state) => state.value);\n  const steps = useStore((state) => state.steps);\n  const context = useTourContext(STEP_NAME);\n\n  const resolvedSideOffset = sideOffset ?? context.sideOffset;\n  const resolvedAlignOffset = alignOffset ?? context.alignOffset;\n\n  useIsomorphicLayoutEffect(() => {\n    const { id, index } = store.addStep({\n      target,\n      align,\n      alignOffset: resolvedAlignOffset,\n      side,\n      sideOffset: resolvedSideOffset,\n      collisionBoundary,\n      collisionPadding,\n      arrowPadding,\n      sticky,\n      hideWhenDetached,\n      avoidCollisions,\n      onStepEnter,\n      onStepLeave,\n      required,\n    });\n    stepIdRef.current = id;\n    stepOrderRef.current = index;\n\n    return () => {\n      store.removeStep(stepIdRef.current);\n    };\n  }, [\n    target,\n    side,\n    resolvedSideOffset,\n    align,\n    resolvedAlignOffset,\n    collisionPadding,\n    arrowPadding,\n    sticky,\n    hideWhenDetached,\n    avoidCollisions,\n    required,\n    onStepEnter,\n    onStepLeave,\n    store,\n  ]);\n\n  const stepData = steps[value];\n  const targetElement = stepData ? getTargetElement(stepData.target) : null;\n\n  const isCurrentStep = stepOrderRef.current === value;\n\n  const middleware = React.useMemo(() => {\n    if (!stepData) return [];\n\n    const mainAxisOffset = stepData.sideOffset ?? resolvedSideOffset;\n    const crossAxisOffset = stepData.alignOffset ?? resolvedAlignOffset;\n\n    const padding =\n      typeof stepData.collisionPadding === \"number\"\n        ? stepData.collisionPadding\n        : {\n            top: stepData.collisionPadding?.top ?? 0,\n            right: stepData.collisionPadding?.right ?? 0,\n            bottom: stepData.collisionPadding?.bottom ?? 0,\n            left: stepData.collisionPadding?.left ?? 0,\n          };\n\n    const boundary = Array.isArray(stepData.collisionBoundary)\n      ? stepData.collisionBoundary\n      : stepData.collisionBoundary\n        ? [stepData.collisionBoundary]\n        : [];\n    const hasExplicitBoundaries = boundary.length > 0;\n\n    const detectOverflowOptions = {\n      padding,\n      boundary: boundary.filter((b): b is Element => b !== null),\n      altBoundary: hasExplicitBoundaries,\n    };\n\n    return [\n      offset({\n        mainAxis: mainAxisOffset,\n        alignmentAxis: crossAxisOffset,\n      }),\n      stepData.avoidCollisions &&\n        shift({\n          mainAxis: true,\n          crossAxis: false,\n          limiter: stepData.sticky === \"partial\" ? limitShift() : undefined,\n          ...detectOverflowOptions,\n        }),\n      stepData.avoidCollisions && flip({ ...detectOverflowOptions }),\n      arrow && onArrow({ element: arrow, padding: stepData.arrowPadding }),\n      stepData.hideWhenDetached &&\n        hide({\n          strategy: \"referenceHidden\",\n          ...detectOverflowOptions,\n        }),\n    ].filter(Boolean) as Middleware[];\n  }, [stepData, resolvedSideOffset, resolvedAlignOffset, arrow]);\n\n  const placement = getPlacement(\n    stepData?.side ?? side,\n    stepData?.align ?? align,\n  );\n\n  const {\n    refs,\n    floatingStyles,\n    placement: finalPlacement,\n    middlewareData,\n  } = useFloating({\n    placement,\n    middleware,\n    strategy: \"fixed\",\n    whileElementsMounted: autoUpdate,\n    elements: {\n      reference: targetElement,\n    },\n  });\n\n  const composedRef = useComposedRefs(refs.setFloating, stepRef);\n\n  const [placedSide, placedAlign] =\n    getSideAndAlignFromPlacement(finalPlacement);\n\n  const arrowX = middlewareData.arrow?.x;\n  const arrowY = middlewareData.arrow?.y;\n  const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;\n  const isHidden = hideWhenDetached && middlewareData.hide?.referenceHidden;\n\n  const stepContextValue = React.useMemo<StepContextValue>(\n    () => ({\n      arrowX,\n      arrowY,\n      placedAlign,\n      placedSide,\n      shouldHideArrow: cannotCenterArrow,\n      onArrowChange: setArrow,\n      onFooterChange: setFooter,\n    }),\n    [arrowX, arrowY, placedSide, placedAlign, cannotCenterArrow],\n  );\n\n  React.useEffect(() => {\n    if (open && targetElement && isCurrentStep) {\n      updateMask(store, targetElement, context.spotlightPadding);\n\n      let rafId: number | null = null;\n\n      function onResize() {\n        if (targetElement) {\n          updateMask(store, targetElement, context.spotlightPadding);\n        }\n      }\n\n      function onScroll() {\n        if (rafId !== null) return;\n        rafId = requestAnimationFrame(() => {\n          if (targetElement) {\n            updateMask(store, targetElement, context.spotlightPadding);\n          }\n          rafId = null;\n        });\n      }\n\n      window.addEventListener(\"resize\", onResize);\n      window.addEventListener(\"scroll\", onScroll, { passive: true });\n      return () => {\n        window.removeEventListener(\"resize\", onResize);\n        window.removeEventListener(\"scroll\", onScroll);\n        if (rafId !== null) {\n          cancelAnimationFrame(rafId);\n        }\n      };\n    }\n  }, [open, targetElement, isCurrentStep, store, context.spotlightPadding]);\n\n  React.useEffect(() => {\n    if (!open || !isCurrentStep) return;\n\n    const stepElement = stepRef.current;\n    if (!stepElement) return;\n\n    const ownerDocument = stepElement.ownerDocument;\n\n    function onPointerDown(event: PointerEvent) {\n      if (event.target && !isPointerInsideReactTreeRef.current) {\n        const pointerDownOutsideEvent = new CustomEvent(POINTER_DOWN_OUTSIDE, {\n          ...EVENT_OPTIONS,\n          detail: { originalEvent: event },\n        });\n\n        context.onPointerDownOutside?.(pointerDownOutsideEvent);\n\n        const interactOutsideEvent = new CustomEvent(INTERACT_OUTSIDE, {\n          ...EVENT_OPTIONS,\n          detail: { originalEvent: event },\n        });\n        context.onInteractOutside?.(interactOutsideEvent);\n\n        if (\n          !pointerDownOutsideEvent.defaultPrevented &&\n          !interactOutsideEvent.defaultPrevented &&\n          context.dismissible\n        ) {\n          store.setState(\"open\", false);\n        }\n      }\n\n      isPointerInsideReactTreeRef.current = false;\n    }\n\n    const timerId = window.setTimeout(() => {\n      ownerDocument.addEventListener(\"pointerdown\", onPointerDown);\n    }, 0);\n\n    return () => {\n      window.clearTimeout(timerId);\n      ownerDocument.removeEventListener(\"pointerdown\", onPointerDown);\n    };\n  }, [open, isCurrentStep, store, context]);\n\n  React.useEffect(() => {\n    if (!open || !isCurrentStep) return;\n\n    const stepElement = stepRef.current;\n    if (!stepElement) return;\n\n    const ownerDocument = stepElement.ownerDocument;\n\n    function onFocusIn(event: FocusEvent) {\n      const target = event.target as HTMLElement;\n\n      const isFocusInStep = stepElement?.contains(target);\n      const isFocusInTarget = targetElement?.contains(target);\n\n      if (\n        event.target &&\n        !isFocusInsideReactTreeRef.current &&\n        !isFocusInStep &&\n        !isFocusInTarget\n      ) {\n        const interactOutsideEvent = new CustomEvent(INTERACT_OUTSIDE, {\n          ...EVENT_OPTIONS,\n          detail: { originalEvent: event },\n        });\n\n        context.onInteractOutside?.(interactOutsideEvent);\n\n        if (!interactOutsideEvent.defaultPrevented && context.dismissible) {\n          store.setState(\"open\", false);\n        }\n      }\n    }\n\n    ownerDocument.addEventListener(\"focusin\", onFocusIn);\n\n    return () => {\n      ownerDocument.removeEventListener(\"focusin\", onFocusIn);\n    };\n  }, [open, isCurrentStep, store, context, targetElement]);\n\n  const onPointerDownCapture = React.useCallback(\n    (event: React.PointerEvent<StepElement>) => {\n      onPointerDownCaptureProp?.(event);\n      isPointerInsideReactTreeRef.current = true;\n    },\n    [onPointerDownCaptureProp],\n  );\n\n  const onFocusCapture = React.useCallback(\n    (event: React.FocusEvent<StepElement>) => {\n      onFocusCaptureProp?.(event);\n      isFocusInsideReactTreeRef.current = true;\n    },\n    [onFocusCaptureProp],\n  );\n\n  const onBlurCapture = React.useCallback(\n    (event: React.FocusEvent<StepElement>) => {\n      onBlurCaptureProp?.(event);\n      isFocusInsideReactTreeRef.current = false;\n    },\n    [onBlurCaptureProp],\n  );\n\n  React.useEffect(() => {\n    if (!open || !isCurrentStep || !targetElement) return;\n\n    function onTargetPointerDownCapture() {\n      isPointerInsideReactTreeRef.current = true;\n    }\n\n    function onTargetFocusCapture() {\n      isFocusInsideReactTreeRef.current = true;\n    }\n\n    function onTargetBlurCapture() {\n      isFocusInsideReactTreeRef.current = false;\n    }\n\n    targetElement.addEventListener(\n      \"pointerdown\",\n      onTargetPointerDownCapture,\n      true,\n    );\n    targetElement.addEventListener(\"focus\", onTargetFocusCapture, true);\n    targetElement.addEventListener(\"blur\", onTargetBlurCapture, true);\n\n    return () => {\n      targetElement.removeEventListener(\n        \"pointerdown\",\n        onTargetPointerDownCapture,\n        true,\n      );\n      targetElement.removeEventListener(\"focus\", onTargetFocusCapture, true);\n      targetElement.removeEventListener(\"blur\", onTargetBlurCapture, true);\n    };\n  }, [open, isCurrentStep, targetElement]);\n\n  useFocusGuards();\n  useFocusTrap(\n    stepRef,\n    open && isCurrentStep,\n    open,\n    context.onOpenAutoFocus,\n    context.onCloseAutoFocus,\n  );\n\n  if (!open || !stepData || (!targetElement && !forceMount) || !isCurrentStep) {\n    return null;\n  }\n\n  const StepPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StepContext.Provider value={stepContextValue}>\n      <StepPrimitive\n        ref={composedRef}\n        data-slot=\"tour-step\"\n        data-side={placedSide}\n        data-align={placedAlign}\n        dir={context.dir}\n        tabIndex={-1}\n        {...stepProps}\n        onPointerDownCapture={onPointerDownCapture}\n        onFocusCapture={onFocusCapture}\n        onBlurCapture={onBlurCapture}\n        className={cn(\n          \"fixed z-50 flex w-80 flex-col gap-4 rounded-lg border bg-popover p-4 text-popover-foreground shadow-md outline-none\",\n          className,\n        )}\n        style={{\n          ...style,\n          ...floatingStyles,\n          visibility: isHidden ? \"hidden\" : undefined,\n          pointerEvents: isHidden ? \"none\" : undefined,\n        }}\n      >\n        {children}\n        {!footer && (\n          <DefaultFooterContext.Provider value={true}>\n            {context.stepFooter}\n          </DefaultFooterContext.Provider>\n        )}\n      </StepPrimitive>\n    </StepContext.Provider>\n  );\n}\n\ninterface TourSpotlightProps extends DivProps {\n  forceMount?: boolean;\n}\n\nfunction TourSpotlight(props: TourSpotlightProps) {\n  const {\n    asChild,\n    className,\n    style,\n    forceMount = false,\n    ...backdropProps\n  } = props;\n\n  const open = useStore((state) => state.open);\n  const maskPath = useStore((state) => state.maskPath);\n\n  if (!open && !forceMount) return null;\n\n  const SpotlightPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SpotlightPrimitive\n      data-slot=\"tour-spotlight\"\n      data-state={getDataState(open)}\n      {...backdropProps}\n      className={cn(\n        \"fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0\",\n        className,\n      )}\n      style={{\n        clipPath: maskPath,\n        ...style,\n      }}\n    />\n  );\n}\n\ninterface TourSpotlightRingProps extends DivProps {\n  forceMount?: boolean;\n}\n\nfunction TourSpotlightRing(props: TourSpotlightRingProps) {\n  const { asChild, className, style, forceMount = false, ...ringProps } = props;\n\n  const open = useStore((state) => state.open);\n  const spotlightRect = useStore((state) => state.spotlightRect);\n\n  if (!open && !forceMount) return null;\n  if (!spotlightRect) return null;\n\n  const RingPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <RingPrimitive\n      data-slot=\"tour-spotlight-ring\"\n      data-state={getDataState(open)}\n      {...ringProps}\n      className={cn(\n        \"pointer-events-none fixed z-50 border-ring ring-[3px] ring-ring/50\",\n        className,\n      )}\n      style={{\n        left: spotlightRect.x,\n        top: spotlightRect.y,\n        width: spotlightRect.width,\n        height: spotlightRect.height,\n        ...style,\n      }}\n    />\n  );\n}\n\ninterface TourPortalProps {\n  children?: React.ReactNode;\n  container?: HTMLElement | null;\n}\n\nfunction TourPortal(props: TourPortalProps) {\n  const { children, container } = props;\n\n  const portalContext = usePortalContext(PORTAL_NAME);\n\n  const [mounted, setMounted] = React.useState(false);\n\n  useIsomorphicLayoutEffect(() => {\n    setMounted(true);\n\n    const node = container ?? document.body;\n\n    portalContext?.onPortalChange(node);\n    return () => {\n      portalContext?.onPortalChange(null);\n    };\n  }, [container, portalContext]);\n\n  if (!mounted) return null;\n\n  const portalContainer = container ?? portalContext?.portal ?? document.body;\n\n  return ReactDOM.createPortal(children, portalContainer);\n}\n\ninterface TourArrowProps extends React.ComponentProps<\"svg\"> {\n  width?: number;\n  height?: number;\n  asChild?: boolean;\n}\n\nfunction TourArrow(props: TourArrowProps) {\n  const {\n    width = 10,\n    height = 5,\n    className,\n    children,\n    asChild,\n    ...arrowProps\n  } = props;\n\n  const stepContext = useStepContext(ARROW_NAME);\n  const baseSide = OPPOSITE_SIDE[stepContext.placedSide];\n\n  return (\n    <span\n      ref={stepContext.onArrowChange}\n      data-slot=\"tour-arrow\"\n      style={{\n        position: \"absolute\",\n        left:\n          stepContext.arrowX != null ? `${stepContext.arrowX}px` : undefined,\n        top: stepContext.arrowY != null ? `${stepContext.arrowY}px` : undefined,\n        [baseSide]: 0,\n        transformOrigin: {\n          top: \"\",\n          right: \"0 0\",\n          bottom: \"center 0\",\n          left: \"100% 0\",\n        }[stepContext.placedSide],\n        transform: {\n          top: \"translateY(100%)\",\n          right: \"translateY(50%) rotate(90deg) translateX(-50%)\",\n          bottom: \"rotate(180deg)\",\n          left: \"translateY(50%) rotate(-90deg) translateX(50%)\",\n        }[stepContext.placedSide],\n        visibility: stepContext.shouldHideArrow ? \"hidden\" : undefined,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 30 10\"\n        preserveAspectRatio=\"none\"\n        width={width}\n        height={height}\n        {...arrowProps}\n        className={cn(\"block fill-popover stroke-border\", className)}\n      >\n        {asChild ? children : <polygon points=\"0,0 30,0 15,10\" />}\n      </svg>\n    </span>\n  );\n}\n\nfunction TourHeader(props: DivProps) {\n  const { asChild, className, ...headerProps } = props;\n\n  const context = useTourContext(HEADER_NAME);\n\n  const HeaderPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <HeaderPrimitive\n      data-slot=\"tour-header\"\n      dir={context.dir}\n      {...headerProps}\n      className={cn(\n        \"flex flex-col gap-1.5 text-center sm:text-left\",\n        className,\n      )}\n    />\n  );\n}\n\nfunction TourTitle(props: DivProps) {\n  const { asChild, className, ...titleProps } = props;\n\n  const context = useTourContext(TITLE_NAME);\n\n  const TitlePrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <TitlePrimitive\n      data-slot=\"tour-title\"\n      dir={context.dir}\n      {...titleProps}\n      className={cn(\n        \"text-lg leading-none font-semibold tracking-tight\",\n        className,\n      )}\n    />\n  );\n}\n\nfunction TourDescription(props: DivProps) {\n  const { asChild, className, ...descriptionProps } = props;\n\n  const context = useTourContext(DESCRIPTION_NAME);\n\n  const DescriptionPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <DescriptionPrimitive\n      data-slot=\"tour-description\"\n      dir={context.dir}\n      {...descriptionProps}\n      className={cn(\"text-sm text-muted-foreground\", className)}\n    />\n  );\n}\n\ninterface TourCloseProps extends React.ComponentProps<\"button\"> {\n  asChild?: boolean;\n}\n\nfunction TourClose(props: TourCloseProps) {\n  const {\n    asChild,\n    className,\n    onClick: onClickProp,\n    ...closeButtonProps\n  } = props;\n\n  const store = useStoreContext(CLOSE_NAME);\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<CloseElement>) => {\n      onClickProp?.(event);\n      if (event.defaultPrevented) return;\n\n      store.setState(\"open\", false);\n    },\n    [store, onClickProp],\n  );\n\n  const ClosePrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n  return (\n    <ClosePrimitive\n      type=\"button\"\n      aria-label=\"Close tour\"\n      className={cn(\n        \"absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      onClick={onClick}\n      {...closeButtonProps}\n    >\n      <IconPlaceholder\n        lucide=\"X\"\n        tabler=\"IconX\"\n        hugeicons=\"Cancel01Icon\"\n        phosphor=\"XIcon\"\n        remixicon=\"RiCloseLine\"\n        className=\"size-4\"\n      />\n    </ClosePrimitive>\n  );\n}\n\nfunction TourPrev(props: React.ComponentProps<typeof Button>) {\n  const { children, onClick: onClickProp, ...prevButtonProps } = props;\n\n  const store = useStoreContext(PREV_NAME);\n  const value = useStore((state) => state.value);\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<PrevElement>) => {\n      onClickProp?.(event);\n      if (event.defaultPrevented) return;\n\n      if (value > 0) {\n        store.setState(\"value\", value - 1);\n      }\n    },\n    [value, store, onClickProp],\n  );\n\n  return (\n    <Button\n      type=\"button\"\n      aria-label=\"Previous step\"\n      data-slot=\"tour-prev\"\n      variant=\"outline\"\n      {...prevButtonProps}\n      onClick={onClick}\n      disabled={value === 0}\n    >\n      {children ?? (\n        <>\n          <IconPlaceholder\n            lucide=\"ChevronLeft\"\n            tabler=\"IconChevronLeft\"\n            hugeicons=\"ArrowLeft01Icon\"\n            phosphor=\"CaretLeftIcon\"\n            remixicon=\"RiArrowLeftSLine\"\n          />\n          Previous\n        </>\n      )}\n    </Button>\n  );\n}\n\nfunction TourNext(props: React.ComponentProps<typeof Button>) {\n  const { children, onClick: onClickProp, ...nextButtonProps } = props;\n  const store = useStoreContext(NEXT_NAME);\n  const value = useStore((state) => state.value);\n  const steps = useStore((state) => state.steps);\n\n  const isLastStep = value === steps.length - 1;\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<NextElement>) => {\n      onClickProp?.(event);\n      if (event.defaultPrevented) return;\n\n      store.setState(\"value\", value + 1);\n    },\n    [value, store, onClickProp],\n  );\n\n  return (\n    <Button\n      type=\"button\"\n      aria-label=\"Next step\"\n      data-slot=\"tour-next\"\n      {...nextButtonProps}\n      onClick={onClick}\n    >\n      {children ?? (\n        <>\n          {isLastStep ? \"Finish\" : \"Next\"}\n          {!isLastStep && (\n            <IconPlaceholder\n              lucide=\"ChevronRight\"\n              tabler=\"IconChevronRight\"\n              hugeicons=\"ArrowRight01Icon\"\n              phosphor=\"CaretRightIcon\"\n              remixicon=\"RiArrowRightSLine\"\n            />\n          )}\n        </>\n      )}\n    </Button>\n  );\n}\n\nfunction TourSkip(props: React.ComponentProps<typeof Button>) {\n  const { children, onClick: onClickProp, ...skipButtonProps } = props;\n\n  const store = useStoreContext(SKIP_NAME);\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<SkipElement>) => {\n      onClickProp?.(event);\n      if (event.defaultPrevented) return;\n\n      store.setState(\"open\", false);\n    },\n    [store, onClickProp],\n  );\n\n  return (\n    <Button\n      type=\"button\"\n      aria-label=\"Skip tour\"\n      data-slot=\"tour-skip\"\n      variant=\"outline\"\n      {...skipButtonProps}\n      onClick={onClick}\n    >\n      {children ?? \"Skip\"}\n    </Button>\n  );\n}\n\ninterface TourStepCounterProps extends DivProps {\n  format?: (current: number, total: number) => string;\n}\n\nfunction TourStepCounter(props: TourStepCounterProps) {\n  const {\n    format = (current, total) => `${current} / ${total}`,\n    asChild,\n    className,\n    children,\n    ...stepCounterProps\n  } = props;\n\n  const value = useStore((state) => state.value);\n  const steps = useStore((state) => state.steps);\n\n  const StepCounterPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StepCounterPrimitive\n      data-slot=\"tour-step-counter\"\n      {...stepCounterProps}\n      className={cn(\"text-sm text-muted-foreground\", className)}\n    >\n      {children ?? format(value + 1, steps.length)}\n    </StepCounterPrimitive>\n  );\n}\n\nfunction TourFooter(props: DivProps) {\n  const { asChild, className, ref, ...footerProps } = props;\n\n  const stepContext = useStepContext(FOOTER_NAME);\n  const hasDefaultFooter = React.useContext(DefaultFooterContext);\n  const context = useTourContext(FOOTER_NAME);\n\n  const composedRef = useComposedRefs(\n    ref,\n    hasDefaultFooter ? undefined : stepContext.onFooterChange,\n  );\n\n  const FooterPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <FooterPrimitive\n      data-slot=\"tour-footer\"\n      dir={context.dir}\n      {...footerProps}\n      ref={composedRef}\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className,\n      )}\n    />\n  );\n}\n\nexport {\n  Tour,\n  TourArrow,\n  TourClose,\n  TourDescription,\n  TourFooter,\n  TourHeader,\n  TourNext,\n  TourPortal,\n  TourPrev,\n  type TourProps,\n  TourSkip,\n  TourSpotlight,\n  TourSpotlightRing,\n  TourStep,\n  TourStepCounter,\n  TourTitle,\n};\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}