{"version":3,"file":"tour-root.cjs","sources":["../../../components/tour/tour-root.tsx"],"sourcesContent":["'use client';\n\nimport { useControlled } from '@base-ui/utils/useControlled';\nimport {\n  type ReactNode,\n  type RefObject,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState\n} from 'react';\nimport { TourContent } from './tour-content';\nimport { TourContext, type TourContextValue } from './tour-context';\nimport { TourOverlay } from './tour-overlay';\nimport type {\n  TourActions,\n  TourEndStatus,\n  TourEvent,\n  TourStatus,\n  TourStep,\n  TourTransition\n} from './types';\nimport { usePrefersReducedMotion } from './use-prefers-reduced-motion';\nimport { resolveTourTarget, useTourTarget } from './use-tour-target';\nimport { rectsEqual } from './utils';\n\n// Must match --rs-duration-fast — the .spotlightCover fade in tour.module.css.\nconst FADE_OUT_MS = 150;\n\nconst REVEAL_TIMEOUT_MS = 2000;\n\nfunction isElementInView(el: Element): boolean {\n  const rect = el.getBoundingClientRect();\n  let node = el.parentElement;\n  while (node && node !== document.documentElement) {\n    const { overflowX, overflowY } = getComputedStyle(node);\n    const scrollable = /(auto|scroll|overlay)/.test(overflowX + overflowY);\n    if (scrollable) {\n      const bounds = node.getBoundingClientRect();\n      if (\n        rect.top < bounds.top ||\n        rect.bottom > bounds.bottom ||\n        rect.left < bounds.left ||\n        rect.right > bounds.right\n      ) {\n        return false;\n      }\n    }\n    node = node.parentElement;\n  }\n  const vw = window.innerWidth;\n  const vh = window.innerHeight;\n  // The second clause handles an oversized target spanning the viewport.\n  const inViewY =\n    (rect.top >= 0 && rect.bottom <= vh) ||\n    (rect.height > vh && rect.top <= 0 && rect.bottom >= vh);\n  const inViewX =\n    (rect.left >= 0 && rect.right <= vw) ||\n    (rect.width > vw && rect.left <= 0 && rect.right >= vw);\n  return inViewX && inViewY;\n}\n\nexport interface TourRootProps {\n  /** Ordered list of steps that make up the tour. */\n  steps: TourStep[];\n  /** Whether the tour is currently open (controlled). */\n  open?: boolean;\n  /** Whether the tour is initially open. @default false */\n  defaultOpen?: boolean;\n  /** Called when the tour opens or closes. `status` is set when closing. */\n  onOpenChange?: (open: boolean, details: { status?: TourEndStatus }) => void;\n  /**\n   * Active step index (controlled). Keep it within `0..steps.length - 1`: the\n   * tour clamps out-of-range values internally for rendering but never writes\n   * the clamped value back, so a controlled parent must not hold one out of\n   * range (its `next`/`prev`/`go` operate on the clamped index).\n   */\n  stepIndex?: number;\n  /** Initially active step when uncontrolled. @default 0 */\n  defaultStepIndex?: number;\n  /** Called when the active step changes. */\n  onStepChange?: (index: number, step: TourStep) => void;\n  /** Receives every tour lifecycle event. */\n  onEvent?: (event: TourEvent) => void;\n  /** A ref populated with the imperative tour controls. */\n  actionsRef?: RefObject<TourActions | null>;\n  /**\n   * How long to wait for a step target to appear in the DOM before giving\n   * up, in ms. @default 5000\n   */\n  targetTimeout?: number;\n  /**\n   * What to do when a step target cannot be found: skip to the next step or\n   * stop the tour. Emits `error:target-not-found` either way.\n   * @default 'skip'\n   */\n  targetNotFound?: 'skip' | 'stop';\n  /**\n   * How the popover card travels between steps. `fade` (default) cross-fades it\n   * at each target; `move` glides it smoothly from one target to the next. The\n   * spotlight always cross-fades regardless — it never slides. @default 'fade'\n   */\n  transition?: TourTransition;\n  /**\n   * Hide the dimmed overlay for the whole tour — only the popover is shown and\n   * the page stays fully interactive. Steps can override with\n   * `step.disableOverlay`. @default false\n   */\n  disableOverlay?: boolean;\n  /**\n   * Tour UI. Defaults to `<Tour.Overlay />` plus `<Tour.Content />` with the\n   * standard card layout; compose the parts to customize.\n   */\n  children?: ReactNode;\n}\n\nexport function TourRoot({\n  steps,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  stepIndex: stepIndexProp,\n  defaultStepIndex = 0,\n  onStepChange,\n  onEvent,\n  actionsRef,\n  targetTimeout = 5000,\n  targetNotFound = 'skip',\n  transition = 'fade',\n  disableOverlay = false,\n  children\n}: TourRootProps) {\n  const [open, setOpenUnwrapped] = useControlled({\n    controlled: openProp,\n    default: defaultOpen,\n    name: 'Tour',\n    state: 'open'\n  });\n  const [indexUnclamped, setIndexUnwrapped] = useControlled({\n    controlled: stepIndexProp,\n    default: defaultStepIndex,\n    name: 'Tour',\n    state: 'stepIndex'\n  });\n  const index = Math.min(\n    Math.max(indexUnclamped, 0),\n    Math.max(steps.length - 1, 0)\n  );\n  const step = open ? (steps[index] ?? null) : null;\n\n  const stepsRef = useRef(steps);\n  stepsRef.current = steps;\n  const indexRef = useRef(index);\n  indexRef.current = index;\n  const openRef = useRef(open);\n  openRef.current = open;\n  const onOpenChangeRef = useRef(onOpenChange);\n  onOpenChangeRef.current = onOpenChange;\n  const onStepChangeRef = useRef(onStepChange);\n  onStepChangeRef.current = onStepChange;\n  const onEventRef = useRef(onEvent);\n  onEventRef.current = onEvent;\n  const targetNotFoundRef = useRef(targetNotFound);\n  targetNotFoundRef.current = targetNotFound;\n  const endStatusRef = useRef<TourEndStatus>('closed');\n\n  const emit = useCallback(\n    (event: TourEvent) => onEventRef.current?.(event),\n    []\n  );\n\n  const setOpen = useCallback(\n    (nextOpen: boolean, status?: TourEndStatus) => {\n      if (status) endStatusRef.current = status;\n      setOpenUnwrapped(nextOpen);\n      onOpenChangeRef.current?.(nextOpen, {\n        status: nextOpen ? undefined : endStatusRef.current\n      });\n    },\n    [setOpenUnwrapped]\n  );\n\n  const setIndex = useCallback(\n    (nextIndex: number) => {\n      setIndexUnwrapped(nextIndex);\n      onStepChangeRef.current?.(nextIndex, stepsRef.current[nextIndex]);\n    },\n    [setIndexUnwrapped]\n  );\n\n  const actions = useMemo<TourActions>(\n    () => ({\n      start: (at = 0) => {\n        const clamped = Math.min(\n          Math.max(at, 0),\n          Math.max(stepsRef.current.length - 1, 0)\n        );\n        setIndex(clamped);\n        setOpen(true);\n      },\n      stop: () => {\n        if (openRef.current) setOpen(false, 'closed');\n      },\n      skip: () => {\n        if (openRef.current) setOpen(false, 'skipped');\n      },\n      next: () => {\n        if (!openRef.current) return;\n        if (indexRef.current >= stepsRef.current.length - 1) {\n          setOpen(false, 'finished');\n        } else {\n          setIndex(indexRef.current + 1);\n        }\n      },\n      prev: () => {\n        if (openRef.current && indexRef.current > 0) {\n          setIndex(indexRef.current - 1);\n        }\n      },\n      go: at => {\n        if (openRef.current && at >= 0 && at < stepsRef.current.length) {\n          setIndex(at);\n        }\n      }\n    }),\n    [setIndex, setOpen]\n  );\n\n  useImperativeHandle(actionsRef, () => actions, [actions]);\n\n  const handleTargetNotFound = useCallback(() => {\n    const at = indexRef.current;\n    emit({\n      type: 'error:target-not-found',\n      index: at,\n      step: stepsRef.current[at]\n    });\n    if (\n      targetNotFoundRef.current === 'stop' ||\n      at >= stepsRef.current.length - 1\n    ) {\n      setOpen(false, 'closed');\n    } else {\n      setIndex(at + 1);\n    }\n  }, [emit, setIndex, setOpen]);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: keyed on open/index, not `step` — inline steps arrays give function targets a fresh identity each render.\n  const target = useMemo(\n    () => (open ? step?.target : undefined),\n    [open, index]\n  );\n  // biome-ignore lint/correctness/useExhaustiveDependencies: keyed on open/index, not `step`.\n  const spotlightTarget = useMemo(\n    () => (open ? step?.spotlightTarget : undefined),\n    [open, index]\n  );\n\n  const { element: anchor, state: targetState } = useTourTarget(target, {\n    enabled: open && step != null,\n    timeout: targetTimeout,\n    onNotFound: handleTargetNotFound\n  });\n  const { element: spotlightOverride } = useTourTarget(spotlightTarget, {\n    enabled: open && spotlightTarget != null,\n    timeout: targetTimeout\n  });\n  const spotlightElement = spotlightOverride ?? anchor;\n\n  const popoverOpen = open && step != null && targetState === 'found';\n  const status: TourStatus = !open\n    ? 'idle'\n    : targetState === 'found'\n      ? 'running'\n      : 'waiting';\n\n  const detachedStep =\n    step != null && step.target == null && step.spotlightTarget == null;\n\n  const reducedMotion = usePrefersReducedMotion();\n  const [revealed, setRevealed] = useState(false);\n  const shownOnceRef = useRef(false);\n\n  // Reset in render (not an effect) so the overlay never sees a stale `revealed`\n  // for a frame; the ref guards against a re-render loop.\n  const revealedForRef = useRef(spotlightElement);\n  if (revealedForRef.current !== spotlightElement) {\n    revealedForRef.current = spotlightElement;\n    if (revealed) setRevealed(false);\n  }\n\n  useEffect(() => {\n    if (!popoverOpen) {\n      setRevealed(false);\n      shownOnceRef.current = false;\n      return;\n    }\n    if (detachedStep) {\n      setRevealed(true);\n      shownOnceRef.current = true;\n      return;\n    }\n    // The fallback reveal is a safety net so a target that never settles can't\n    // hang the tour.\n    setRevealed(false);\n    const el = spotlightElement;\n    const grace = shownOnceRef.current && !reducedMotion ? FADE_OUT_MS : 0;\n    const start = performance.now();\n    let frame = 0;\n    let fallback: ReturnType<typeof setTimeout>;\n    let stable = 0;\n    let last: DOMRect | null = null;\n    let settled = false;\n    const reveal = () => {\n      if (settled) return;\n      settled = true;\n      cancelAnimationFrame(frame);\n      clearTimeout(fallback);\n      setRevealed(true);\n      shownOnceRef.current = true;\n    };\n    const check = () => {\n      if (el?.isConnected && isElementInView(el)) {\n        const next = el.getBoundingClientRect();\n        if (last && rectsEqual(last, next)) {\n          stable += 1;\n        } else {\n          stable = 0;\n        }\n        last = next;\n        if (stable >= 2 && performance.now() - start >= grace) {\n          reveal();\n          return;\n        }\n      } else {\n        stable = 0;\n      }\n      frame = requestAnimationFrame(check);\n    };\n    fallback = setTimeout(reveal, REVEAL_TIMEOUT_MS);\n    frame = requestAnimationFrame(check);\n    return () => {\n      cancelAnimationFrame(frame);\n      clearTimeout(fallback);\n    };\n  }, [popoverOpen, detachedStep, spotlightElement, reducedMotion]);\n\n  // Starts false so a tour mounted already-open still emits `tour:start`.\n  const prevOpenRef = useRef(false);\n  useEffect(() => {\n    if (prevOpenRef.current === open) return;\n    prevOpenRef.current = open;\n    if (open) {\n      emit({\n        type: 'tour:start',\n        index: indexRef.current,\n        step: stepsRef.current[indexRef.current]\n      });\n    } else {\n      emit({\n        type: 'tour:end',\n        index: indexRef.current,\n        status: endStatusRef.current\n      });\n      endStatusRef.current = 'closed';\n    }\n  }, [open, emit]);\n\n  const lastActiveIndexRef = useRef(-1);\n  useEffect(() => {\n    if (!open) {\n      lastActiveIndexRef.current = -1;\n      return;\n    }\n    if (!popoverOpen || !step || lastActiveIndexRef.current === index) return;\n    lastActiveIndexRef.current = index;\n    emit({ type: 'step:active', index, step });\n  }, [open, popoverOpen, index, step, emit]);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: keyed on `index`, not `step` — the per-render `step` identity would re-fire this and fight the user's scroll.\n  useEffect(() => {\n    if (!popoverOpen || !step || step.disableScroll) return;\n    const el = resolveTourTarget(step.scrollTarget) ?? anchor;\n    if (!el?.isConnected) return;\n    if (isElementInView(el)) return;\n    el.scrollIntoView({\n      block: 'center',\n      inline: 'nearest',\n      behavior: reducedMotion ? 'auto' : 'smooth'\n    });\n  }, [popoverOpen, index, anchor, reducedMotion]);\n\n  const contextValue = useMemo<TourContextValue>(\n    () => ({\n      steps,\n      index,\n      step,\n      open,\n      status,\n      anchor,\n      spotlightElement,\n      popoverOpen,\n      disableOverlay,\n      transition,\n      revealed,\n      actions\n    }),\n    [\n      steps,\n      index,\n      step,\n      open,\n      status,\n      anchor,\n      spotlightElement,\n      popoverOpen,\n      disableOverlay,\n      transition,\n      revealed,\n      actions\n    ]\n  );\n\n  return (\n    <TourContext.Provider value={contextValue}>\n      {children ?? (\n        <>\n          <TourOverlay />\n          <TourContent />\n        </>\n      )}\n    </TourContext.Provider>\n  );\n}\n\nTourRoot.displayName = 'Tour';\n"],"names":[],"mappings":";;;;;;;;;;;;;AA4BA;AACA;AAEA;AAEA;AACE;AACA;;;;;AAKI;AACA;AAEE;AACA;AACA;AAEA;;;AAGJ;;AAEF;AACA;;AAEA;AAEE;AACF;AAEE;;AAEJ;AAwDM;AAgBJ;AACE;AACA;AACA;AACA;AACD;AACD;AACE;AACA;AACA;AACA;AACD;AACD;AAIA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;;AAOI;AAAY;;AAEZ;;AAEC;AACH;AAIF;;AAGI;AACF;AAIF;AAEI;AACE;;;;;;AAQqB;;;;AAGA;;;;;AAIrB;AACE;;;AAEA;;;;;AAKA;;;;AAIF;;;;AAIH;AAIH;AAEA;AACE;AACA;AACE;AACA;AACA;AACD;AACD;;AAIE;;;AAEA;;;;;;;AAeJ;AACE;AACA;AACA;AACD;;AAEC;AACA;AACD;AACD;;;AAIE;;AAEE;;AAGJ;AAGA;;AAEA;;;AAIA;AACA;AACE;AACA;;;;;;AAME;;;;;AAKA;;;;;;;AAOF;AACA;;AAEA;;;;;AAKE;;;;;;AAKA;AACF;;;AAGI;;;;;;;;AAOA;AACE;;;;;;;AAMJ;AACF;AACA;AACA;AACA;;;AAGA;;;AAIF;;AAEE;;AACA;;AAEE;AACE;;;AAGD;;;AAED;AACE;;;AAGD;AACD;;AAEJ;AAEA;;;AAGI;;;;;AAIF;;AAEF;;;;;;;;;;;AASI;AACA;;AAED;;AAGH;;;;;;;;;;;;;AAcG;;;;;;;;;;;;;AAcA;;AAaL;AAEA;;"}