{ "name": "tour", "type": "registry:ui", "files": [ { "path": "ui/tour.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { useDirection } from \"@base-ui/react/direction-provider\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\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 { ChevronLeft, ChevronRight, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/base/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/base/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/base/hooks/use-lazy-ref\";\nimport { Button } from \"@/registry/bases/base/ui/button\";\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\n extends Omit, \"children\">,\n useRender.ComponentProps<\"div\"> {}\n\ntype StepElement = HTMLDivElement;\ntype CloseElement = HTMLButtonElement;\ntype PrevElement = HTMLButtonElement;\ntype NextElement = HTMLButtonElement;\ntype SkipElement = HTMLButtonElement;\ntype FooterElement = HTMLDivElement;\n\nconst OPPOSITE_SIDE: Record = {\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,\n enabled: boolean,\n tourOpen: boolean,\n onOpenAutoFocus?: (event: OpenAutoFocusEvent) => void,\n onCloseAutoFocus?: (event: CloseAutoFocusEvent) => void,\n) {\n const lastFocusedElementRef = React.useRef(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;\n align?: Align;\n alignOffset?: number;\n side?: Side;\n sideOffset?: number;\n collisionBoundary?: Boundary | Boundary[];\n collisionPadding?: number | Partial>;\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: (\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(\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,\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 = {\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(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(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(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(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>;\ntype CloseAutoFocusEvent = CustomEvent>;\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 render,\n ...rootProps\n } = props;\n\n const contextDir = useDirection();\n const dir = dirProp ?? contextDir;\n\n const [portal, setPortal] = React.useState(null);\n const prevOpenRef = React.useRef(undefined);\n const previouslyFocusedElementRef = React.useRef(null);\n\n const stateRef = useLazyRef(() => ({\n open: openProp ?? defaultOpen,\n value: valueProp ?? defaultValue,\n steps: [],\n maskPath: \"\",\n spotlightRect: null,\n }));\n const listenersRef = useLazyRef void>>(() => new Set());\n const stepIdsMapRef = useLazyRef>(() => 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(\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(\n () => ({\n portal,\n onPortalChange: setPortal,\n }),\n [portal],\n );\n\n useScrollLock(open && modal);\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">({ dir }, rootProps),\n render,\n state: {\n slot: \"tour\",\n },\n });\n\n return (\n \n \n \n {element}\n \n \n \n );\n}\n\ninterface TourStepProps extends DivProps {\n target: string | React.RefObject | HTMLElement;\n side?: Side;\n sideOffset?: number;\n align?: Align;\n alignOffset?: number;\n collisionBoundary?: Boundary | Boundary[];\n collisionPadding?: number | Partial>;\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 render,\n ...stepProps\n } = props;\n\n const store = useStoreContext(STEP_NAME);\n\n const [arrow, setArrow] = React.useState(null);\n const [footer, setFooter] = React.useState(null);\n\n const stepRef = React.useRef(null);\n const stepIdRef = React.useRef(\"\");\n const stepOrderRef = React.useRef(-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(\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) => {\n onPointerDownCaptureProp?.(event);\n isPointerInsideReactTreeRef.current = true;\n },\n [onPointerDownCaptureProp],\n );\n\n const onFocusCapture = React.useCallback(\n (event: React.FocusEvent) => {\n onFocusCaptureProp?.(event);\n isFocusInsideReactTreeRef.current = true;\n },\n [onFocusCaptureProp],\n );\n\n const onBlurCapture = React.useCallback(\n (event: React.FocusEvent) => {\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 const stepChildren = React.useMemo(\n () => (\n <>\n {children}\n {!footer && (\n \n {context.stepFooter}\n \n )}\n \n ),\n [children, footer, context.stepFooter],\n );\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n ref: composedRef,\n dir: context.dir,\n tabIndex: -1,\n onPointerDownCapture,\n onFocusCapture,\n 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 children: stepChildren,\n },\n stepProps,\n ),\n render,\n state: {\n slot: \"tour-step\",\n side: placedSide,\n align: placedAlign,\n },\n });\n\n if (!open || !stepData || (!targetElement && !forceMount) || !isCurrentStep) {\n return null;\n }\n\n return (\n \n {element}\n \n );\n}\n\ninterface TourSpotlightProps extends DivProps {\n forceMount?: boolean;\n}\n\nfunction TourSpotlight(props: TourSpotlightProps) {\n const {\n render,\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 const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className,\n ),\n style: {\n clipPath: maskPath,\n ...style,\n },\n },\n backdropProps,\n ),\n render,\n state: {\n slot: \"tour-spotlight\",\n state: getDataState(open),\n },\n });\n\n if (!open && !forceMount) return null;\n\n return element;\n}\n\ninterface TourSpotlightRingProps extends DivProps {\n forceMount?: boolean;\n}\n\nfunction TourSpotlightRing(props: TourSpotlightRingProps) {\n const { render, className, style, forceMount = false, ...ringProps } = props;\n\n const open = useStore((state) => state.open);\n const spotlightRect = useStore((state) => state.spotlightRect);\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"pointer-events-none fixed z-50 border-ring ring-[3px] ring-ring/50\",\n className,\n ),\n style: spotlightRect\n ? {\n left: spotlightRect.x,\n top: spotlightRect.y,\n width: spotlightRect.width,\n height: spotlightRect.height,\n ...style,\n }\n : style,\n },\n ringProps,\n ),\n render,\n state: {\n slot: \"tour-spotlight-ring\",\n state: getDataState(open),\n },\n });\n\n if (!open && !forceMount) return null;\n if (!spotlightRect) return null;\n\n return element;\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 \n \n {asChild ? children : }\n \n \n );\n}\n\nfunction TourHeader(props: DivProps) {\n const { render, className, ...headerProps } = props;\n\n const context = useTourContext(HEADER_NAME);\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n dir: context.dir,\n className: cn(\n \"flex flex-col gap-1.5 text-center sm:text-left\",\n className,\n ),\n },\n headerProps,\n ),\n render,\n state: {\n slot: \"tour-header\",\n },\n });\n}\n\nfunction TourTitle(props: DivProps) {\n const { render, className, ...titleProps } = props;\n\n const context = useTourContext(TITLE_NAME);\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n dir: context.dir,\n className: cn(\n \"font-semibold text-lg leading-none tracking-tight\",\n className,\n ),\n },\n titleProps,\n ),\n render,\n state: {\n slot: \"tour-title\",\n },\n });\n}\n\nfunction TourDescription(props: DivProps) {\n const { render, className, ...descriptionProps } = props;\n\n const context = useTourContext(DESCRIPTION_NAME);\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n dir: context.dir,\n className: cn(\"text-muted-foreground text-sm\", className),\n },\n descriptionProps,\n ),\n render,\n state: {\n slot: \"tour-description\",\n },\n });\n}\n\ninterface TourCloseProps\n extends Omit, \"children\">,\n useRender.ComponentProps<\"button\"> {}\n\nfunction TourClose(props: TourCloseProps) {\n const {\n render,\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) => {\n onClickProp?.(event);\n if (event.defaultPrevented) return;\n\n store.setState(\"open\", false);\n },\n [store, onClickProp],\n );\n\n return useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(\n {\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:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n className,\n ),\n onClick,\n children: ,\n },\n closeButtonProps,\n ),\n render,\n state: {},\n });\n}\n\ninterface TourPrevProps\n extends Omit, \"onClick\"> {\n onClick?: (event: React.MouseEvent) => void;\n}\n\nfunction TourPrev(props: TourPrevProps) {\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) => {\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 \n {children ?? (\n <>\n \n Previous\n \n )}\n \n );\n}\n\ninterface TourNextProps\n extends Omit, \"onClick\"> {\n onClick?: (event: React.MouseEvent) => void;\n}\n\nfunction TourNext(props: TourNextProps) {\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) => {\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 \n {children ?? (\n <>\n {isLastStep ? \"Finish\" : \"Next\"}\n {!isLastStep && }\n \n )}\n \n );\n}\n\ninterface TourSkipProps\n extends Omit, \"onClick\"> {\n onClick?: (event: React.MouseEvent) => void;\n}\n\nfunction TourSkip(props: TourSkipProps) {\n const { children, onClick: onClickProp, ...skipButtonProps } = props;\n\n const store = useStoreContext(SKIP_NAME);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n onClickProp?.(event);\n if (event.defaultPrevented) return;\n\n store.setState(\"open\", false);\n },\n [store, onClickProp],\n );\n\n return (\n \n {children ?? \"Skip\"}\n \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 render,\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 return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\"text-muted-foreground text-sm\", className),\n children: children ?? format(value + 1, steps.length),\n },\n stepCounterProps,\n ),\n render,\n state: {\n slot: \"tour-step-counter\",\n },\n });\n}\n\nfunction TourFooter(props: DivProps) {\n const { render, 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 return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n ref: composedRef,\n dir: context.dir,\n className: cn(\n \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n className,\n ),\n },\n footerProps,\n ),\n render,\n state: {\n slot: \"tour-footer\",\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 = React.Ref | 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(ref: PossibleRef, 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(...refs: PossibleRef[]): React.RefCallback {\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(...refs: PossibleRef[]): React.RefCallback {\n // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", "target": "" } ], "registryDependencies": [ "button", "direction", "@diceui/use-as-ref", "@diceui/use-isomorphic-layout-effect", "@diceui/use-lazy-ref" ], "dependencies": [ "@base-ui/react", "@floating-ui/react-dom" ] }