{"owner":"gilbarbara","repo":"react-joyride","hasSkills":true,"totalSkillsCount":7,"totalTokensCount":16955,"categories":["anthropic-skill","claude-rule","project-spec"],"hasMcp":false,"mcpConfig":null,"found":["skills/react-joyride/references/api-step-state-controls.md","skills/react-joyride/references/api-events-components.md","CLAUDE.md","docs/architecture.md","skills/react-joyride/references/api-props-options.md","skills/react-joyride/references/patterns.md","skills/react-joyride/SKILL.md"],"skills":{"skills/react-joyride/references/api-step-state-controls.md":"# Step, State, Controls\n\n## Step\n\nA single step provided by the user. Extends `SharedProps` and `Partial<Options>`.\n\n```typescript\ntype Step = SharedProps & Partial<Options> & {\n  // Required\n  content: ReactNode;                           // Tooltip body\n  target: StepTarget;                           // Element to highlight\n\n  // Optional\n  id?: string;                                  // Unique identifier\n  title?: ReactNode;                            // Tooltip title\n  data?: any;                                   // Custom data (passed to callbacks)\n\n  // Positioning\n  placement?: Placement | 'auto' | 'center';   // Default: 'bottom'\n  beaconPlacement?: Placement;                  // Beacon-specific placement\n  isFixed?: boolean;                            // Force fixed positioning (default: false)\n\n  // Alternate targets\n  scrollTarget?: StepTarget;                    // Scroll to this instead of target\n  spotlightTarget?: StepTarget;                 // Highlight this instead of target\n}\n```\n\nAll `Options` fields (see api-props-options.md) are also valid on Step for per-step overrides.\n\n## StepTarget\n\nFour ways to specify a target element:\n\n```typescript\ntype StepTarget =\n  | string                              // CSS selector\n  | HTMLElement                         // Direct element reference\n  | RefObject<HTMLElement | null>       // React ref\n  | (() => HTMLElement | null);         // Function (re-evaluated each lifecycle)\n```\n\n## StepMerged\n\nThe normalized step after defaults are applied. All `Options` fields become required, plus:\n- `spotlightPadding: Required<SpotlightPadding>` (always `{ top, right, bottom, left }`)\n- `styles: Styles` (fully resolved)\n\nThis is what you receive in event callbacks and custom component props.\n\n## State\n\n```typescript\ntype State = {\n  action: Actions;         // What triggered the update (init, start, next, prev, etc.)\n  controlled: boolean;     // true when stepIndex prop is set\n  index: number;           // Current step index\n  lifecycle: Lifecycle;     // Step rendering phase\n  origin: Origin | null;   // UI element that triggered action\n  scrolling: boolean;      // Scroll animation in progress\n  size: number;            // Total number of steps\n  status: Status;          // Tour status (idle, ready, running, etc.)\n  waiting: boolean;        // Blocked on before hook or target polling\n}\n```\n\n## Controls\n\nAll 10 methods for programmatic tour control:\n\n```typescript\ntype Controls = {\n  close(origin?: Origin | null): void;\n  // Close the current step and advance to the next one.\n  // Optional origin for event tracking.\n\n  go(nextIndex: number): void;\n  // Jump to a specific step by index.\n  // Uncontrolled mode only — logs warning in controlled mode.\n\n  info(): State;\n  // Get the current tour state snapshot.\n\n  next(): void;\n  // Advance to the next step.\n\n  open(): void;\n  // Open the tooltip for the current step (skip beacon).\n\n  prev(): void;\n  // Go back to the previous step.\n\n  reset(restart?: boolean): void;\n  // Reset the tour to the beginning.\n  // If restart=true, also starts the tour.\n  // Uncontrolled mode only — logs warning in controlled mode.\n\n  skip(origin?: 'button_close' | 'button_skip'): void;\n  // Skip (end) the tour entirely. Sets status to SKIPPED.\n\n  start(nextIndex?: number): void;\n  // Start the tour. Optionally at a specific step index.\n\n  stop(advance?: boolean): void;\n  // Stop (pause) the tour. Sets status to PAUSED.\n  // If advance=true, moves to next step before stopping.\n}\n```\n\n## UseJoyrideReturn\n\n```typescript\ntype UseJoyrideReturn = {\n  controls: Controls;\n  // Programmatic tour control methods.\n\n  failures: StepFailure[];\n  // Steps that failed (target not found, before hook errors).\n  // Clears on start/reset.\n\n  on: (eventType: Events, handler: EventHandler) => () => void;\n  // Subscribe to a specific event type.\n  // Returns an unsubscribe function.\n\n  state: State;\n  // Current tour state (reactive, updates on changes).\n\n  step: StepMerged | null;\n  // Current merged step, or null if no step is active.\n\n  Tour: ReactElement | null;\n  // The tour React element. Render this in your JSX.\n}\n```\n\n## StepFailure\n\n```typescript\ninterface StepFailure {\n  reason: 'before_hook' | 'target_not_found';\n  step: StepMerged;\n}\n```\n\n## Placement\n\n```typescript\ntype Placement =\n  | 'top' | 'top-start' | 'top-end'\n  | 'bottom' | 'bottom-start' | 'bottom-end'\n  | 'left' | 'left-start' | 'left-end'\n  | 'right' | 'right-start' | 'right-end';\n\n// Step also accepts: 'auto' (Floating UI auto-placement) and 'center' (modal-style)\n```\n","skills/react-joyride/references/api-events-components.md":"# Events, Constants, Custom Components\n\n## Constants\n\n### ACTIONS\n```typescript\nconst ACTIONS = {\n  INIT: 'init',\n  START: 'start',\n  STOP: 'stop',\n  RESET: 'reset',\n  PREV: 'prev',\n  NEXT: 'next',\n  GO: 'go',\n  CLOSE: 'close',\n  SKIP: 'skip',\n  UPDATE: 'update',\n  COMPLETE: 'complete',\n} as const;\n```\n\n### EVENTS\n```typescript\nconst EVENTS = {\n  TOUR_START: 'tour:start',\n  STEP_BEFORE_HOOK: 'step:before_hook',\n  STEP_BEFORE: 'step:before',\n  SCROLL_START: 'scroll:start',\n  SCROLL_END: 'scroll:end',\n  BEACON: 'beacon',\n  TOOLTIP: 'tooltip',\n  STEP_AFTER: 'step:after',\n  STEP_AFTER_HOOK: 'step:after_hook',\n  TOUR_END: 'tour:end',\n  TOUR_STATUS: 'tour:status',\n  TARGET_NOT_FOUND: 'error:target_not_found',\n  ERROR: 'error',\n} as const;\n```\n\n### LIFECYCLE\n```typescript\nconst LIFECYCLE = {\n  INIT: 'init',\n  READY: 'ready',\n  BEACON_BEFORE: 'beacon_before',\n  BEACON: 'beacon',\n  TOOLTIP_BEFORE: 'tooltip_before',\n  TOOLTIP: 'tooltip',\n  COMPLETE: 'complete',\n} as const;\n```\n\n### STATUS\n```typescript\nconst STATUS = {\n  IDLE: 'idle',\n  READY: 'ready',\n  WAITING: 'waiting',\n  RUNNING: 'running',\n  PAUSED: 'paused',\n  SKIPPED: 'skipped',\n  FINISHED: 'finished',\n} as const;\n```\n\n### ORIGIN\n```typescript\nconst ORIGIN = {\n  BUTTON_CLOSE: 'button_close',\n  BUTTON_SKIP: 'button_skip',\n  BUTTON_PRIMARY: 'button_primary',\n  KEYBOARD: 'keyboard',\n  OVERLAY: 'overlay',\n} as const;\n```\n\n## Event Types\n\n### EventData\n\nThe full payload passed to `onEvent` and event subscribers:\n\n```typescript\ntype EventData = TourData & {\n  error: Error | null;         // Only for 'error' events\n  scroll: ScrollData | null;   // Only for 'scroll:start' / 'scroll:end'\n  scrolling: boolean;\n  type: Events;                // The event type (discriminator)\n  waiting: boolean;\n}\n```\n\n### TourData\n\nBase payload (also passed to `before` and `after` hooks):\n\n```typescript\ninterface TourData {\n  action: Actions;\n  controlled: boolean;\n  index: number;\n  lifecycle: Lifecycle;\n  origin: Origin | null;\n  size: number;\n  status: Status;\n  step: StepMerged;\n}\n```\n\n### EventHandler\n\n```typescript\ntype EventHandler = (data: EventData, controls: Controls) => void;\n```\n\n### ScrollData\n\n```typescript\ntype ScrollData = {\n  duration: number;     // Scroll duration in ms\n  element: Element;     // Element being scrolled\n  initial: number;      // Scroll position before\n  target: number;       // Computed scroll destination\n}\n```\n\n## Event Flow (per step)\n\n```\ntour:start (once)\n  |\n  v\nstep:before_hook  (if before hook exists)\n  |\n  v\nstep:before  (target found, step about to render)\n  |\n  v\nscroll:start / scroll:end  (if scrolling needed)\n  |\n  v\nbeacon  (if beacon shown) OR tooltip  (if beacon skipped)\n  |\n  v  (user clicks beacon)\ntooltip\n  |\n  v  (user clicks next/prev/close/skip)\nstep:after\n  |\n  v\nstep:after_hook  (if after hook exists)\n  |\n  v  (next step or tour end)\ntour:end  (when finished/skipped)\n```\n\n## Custom Component Render Props\n\n### TooltipRenderProps\n\n```typescript\ntype TooltipRenderProps = {\n  // Tour state\n  continuous: boolean;\n  index: number;\n  isLastStep: boolean;\n  size: number;\n  step: StepMerged;\n\n  // Button props (spread on button elements)\n  backProps: {\n    'aria-label': string;\n    'data-action': string;\n    onClick: MouseEventHandler<HTMLElement>;\n    role: string;\n    title: string;\n  };\n  closeProps: { /* same shape */ };\n  primaryProps: { /* same shape */ };\n  skipProps: { /* same shape */ };\n\n  // Container props (spread on tooltip wrapper)\n  tooltipProps: {\n    'aria-modal': boolean;\n    role: string;  // 'dialog'\n  };\n\n  // Tour controls\n  controls: Controls;\n}\n```\n\n**Important**: Always spread `tooltipProps` on the root element and button props on their respective buttons. This ensures correct accessibility attributes and action handling.\n\n### BeaconRenderProps\n\n```typescript\ntype BeaconRenderProps = {\n  continuous: boolean;\n  index: number;\n  isLastStep: boolean;\n  size: number;\n  step: StepMerged;\n}\n```\n\nMust render a `<span>` since it's placed inside a `<button>` wrapper.\n\n### ArrowRenderProps\n\n```typescript\ntype ArrowRenderProps = {\n  base: number;          // Arrow base width\n  placement: Placement;  // Computed placement\n  size: number;          // Arrow height\n}\n```\n\n### LoaderRenderProps\n\n```typescript\ntype LoaderRenderProps = {\n  step: StepMerged;\n}\n```\n\nSet `loaderComponent={null}` on Props or Step to disable the loader entirely.\n\nDocs: https://v3.react-joyride.com/docs/custom-components\n","CLAUDE.md":"# CLAUDE.md\n\n## Overview\n\nReact Joyride is a React library for creating guided tours (tooltips, walkthroughs, onboarding). Supports React 16.8-19 and SSR.\n\nFor deep architecture details, see `docs/architecture.md`.\n\n## Commands\n\nPackage manager: **pnpm**\n\n```bash\npnpm lint              # ESLint with --fix on src/, test/, and e2e/\npnpm typecheck         # tsc using test/tsconfig.json\npnpm test              # Vitest (run once)\npnpm test:coverage     # With coverage\npnpm test:watch        # Watch mode\npnpm e2e               # Playwright (chromium, firefox, webkit)\npnpm e2e:chromium      # Chromium only\npnpm website           # Website at localhost:3000\npnpm website:build     # Build website for production\npnpm website:start     # Serve built website locally\npnpm website:serve     # Build + start website\npnpm build             # tsdown\npnpm check             # lint → typecheck → test:coverage\npnpm validate          # check → build → size → typevalidation\n```\n\nSingle test: `pnpm test modules/store` | Single e2e: `pnpm e2e:chromium standard`\n\n## How It Works\n\nSingleton `Store` class (ref-based) manages frozen state snapshots. React subscribes via `useSyncExternalStore`.\n\nTwo public APIs: `<Joyride>` component and `useJoyride()` hook (returns `{ controls, failures, on, state, step, Tour }`).\n\n**State machine** has two dimensions: tour `Status` (`idle → ready → waiting → running ↔ paused → finished/skipped`) and step `Lifecycle` (`init → ready → beacon_before → beacon → tooltip_before → tooltip → complete`). See architecture.md for full details.\n\n**Event system**: Single `onEvent(data, controls)` callback receives discriminated events and tour controls: `tour:start`, `step:before_hook`, `step:before`, `scroll:start`, `scroll:end`, `beacon`, `tooltip`, `step:after`, `step:after_hook`, `tour:end`, `tour:status`, `error:target_not_found`, `error`.\n\n**Controlled** (with `stepIndex` prop): Tour pauses at COMPLETE; parent manages index via `onEvent`. **Uncontrolled**: Store manages index internally; supports `initialStepIndex`.\n\n## Key Files\n\n| File | Role |\n|------|------|\n| `src/index.tsx` | SSR-safe wrapper, exports types and literals |\n| `src/hooks/useJoyride.tsx` | Public hook: orchestrates tour, returns controls/state/Tour |\n| `src/hooks/useTourEngine.ts` | Store init, subscriptions, delegates to sub-hooks |\n| `src/hooks/useControls.ts` | Tour control methods (next, prev, start, stop, etc.) |\n| `src/hooks/useEventEmitter.ts` | Centralized event emission with controls |\n| `src/hooks/usePropSync.ts` | Syncs run/steps props to store |\n| `src/hooks/useLifecycleEffect.ts` | 5 effects: action tracking, target resolution, presentation, transitions, tour flow |\n| `src/hooks/useScrollEffect.ts` | Scroll-to-target with placement adjustments |\n| `src/hooks/usePortalElement.ts` | Portal DOM element management |\n| `src/hooks/useTargetPosition.ts` | Target rect tracking via ResizeObserver |\n| `src/hooks/useFocusTrap.ts` | Focus trap for tooltip accessibility |\n| `src/modules/store.ts` | State machine + frozen snapshots |\n| `src/modules/dom.ts` | Element lookup, scroll, visibility |\n| `src/modules/step.ts` | Step merging + validation |\n| `src/modules/helpers.tsx` | Utilities (deepMerge, mergeProps, logDebug) |\n| `src/modules/changes.ts` | State transition detection (treeChanges) |\n| `src/modules/svg.ts` | SVG path generation for overlay/spotlight |\n| `src/defaults.ts` | Default props, locale, options |\n| `src/styles.ts` | Style generation from step |\n| `src/literals/index.ts` | Constants (ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS) |\n\n## Components\n\n| Component | File | Purpose |\n|-----------|------|---------|\n| TourRenderer | `src/components/TourRenderer.tsx` | Conditional rendering, keyboard handler (ESC) |\n| Step | `src/components/Step.tsx` | Validates step + target, renders Floater |\n| Floater | `src/components/Floater.tsx` | Dual Floating UI instances (beacon + tooltip), arrow |\n| Beacon | `src/components/Beacon.tsx` | Animated pulsing indicator |\n| Tooltip | `src/components/Tooltip/index.tsx` | Button handlers, custom component support |\n| DefaultTooltip | `src/components/Tooltip/DefaultTooltip.tsx` | Default tooltip layout |\n| CloseButton | `src/components/Tooltip/CloseButton.tsx` | SVG close icon |\n| Arrow | `src/components/Arrow.tsx` | Tooltip arrow, customizable via `arrowComponent` |\n| Overlay | `src/components/Overlay.tsx` | SVG-based spotlight with path cutout |\n| Loader | `src/components/Loader.tsx` | Loading indicator during target wait/before hooks |\n| Portal | `src/components/Portal.tsx` | React portal wrapper |\n\n## Types\n\nSplit into 8 domain files under `src/types/`: `common`, `components`, `events`, `floating`, `props`, `state`, `step`, `utilities`. Re-exported from `src/types/index.ts`.\n\n## Path Alias\n\n`~/*` maps to `src/*` (tsconfig.json; vitest via native `resolve.tsconfigPaths`; playwright uses website webServer).\n\n## Test Structure\n\n- **Unit tests** (Vitest + jsdom): `test/modules/`, `test/components/`, `test/hooks/`, `test/tours/`\n- **E2E tests** (Playwright): `e2e/` with screenshot snapshots per browser\n- **Fixtures**: `test/__fixtures__/` — Demo components used by both suites\n\nCoverage thresholds: 90% statements, 80% branches, 90% functions, 90% lines.\n\n## Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `@floating-ui/react-dom` | Tooltip/beacon positioning |\n| `@fastify/deepmerge` | Deep object merging (React element aware) |\n| `@gilbarbara/hooks` | useMemoDeepCompare, useMount, usePrevious, useUpdateEffect, useWindowSize |\n| `@gilbarbara/deep-equal` | Deep equality checking |\n| `scroll` / `scrollparent` | Smooth scrolling and scroll parent detection |\n| `react-innertext` | Extract text from React elements |\n| `is-lite` | Type checking utilities |\n\n## Website\n\nNext.js 16 documentation and demo site in `website/`. Imports library via local path (`\"react-joyride\": \"../\"`).\n\n**Stack**: HeroUI v2, Tailwind CSS v4, MDX (rehype-pretty-code + Shiki), next-themes, DocSearch.\n\n**Bundler**: `dev` runs on Turbopack; `build` stays on `--webpack` (`dev:webpack` is the fallback). `next build --turbopack` fails: `@next/mdx`'s Turbopack path never applies the `isServer` vendored-RSC-React alias (its `webpack()` hook does), so server-evaluated modules get the `react-server` React and any interactive HeroUI component's `@react-aria/ssr` `createContext` throws app-wide. The webpack alias in `next.config.ts` masks this; Turbopack has no server-only alias equivalent (a global `resolveAlias` pulls server React into the client bundle). Upstream: nextui-org/nextui#3967, shuding/nextra#4703.\n\n**Documentation** (`website/src/app/docs/`): MDX pages covering getting-started, new-in-v3, how-it-works, hook, props (options, styles, floating-options), step, events, custom-components, exports, accessibility, migration.\n\n**Demos** (`website/src/app/demos/`): 8 interactive examples — overview, controlled, custom-components, scroll, modal, carousel, chat, multi-route.\n\n**Key patterns**:\n- `ConfigContext` — centralized tour configuration for demos\n- `website/src/components/` — 33 components (ConfigPanel, custom tour components, code blocks, navigation)\n- `website/src/config/` — sidebar nav, default values, hero content\n\n## Visual Customization\n- Visual/cosmetic options (colors, borders, sizes) belong in `styles.*`, not as new Options fields.\n- Before adding a new option for appearance, check if it fits an existing or new `styles` key.\n","docs/architecture.md":"# React Joyride Architecture\n\n## Data Flow\n\n```mermaid\nflowchart TD\n    Props --> useJoyride\n    useJoyride --> useTourEngine\n    useTourEngine --> Store\n    Store --> useSyncExternalStore\n    useSyncExternalStore --> TourRenderer\n    TourRenderer --> Step\n    TourRenderer --> Portal\n    Step --> Floater --> Portal\n    Portal --> Overlay & Loader & Beacon & Tooltip\n```\n\nTwo public APIs:\n- `<Joyride>` component — SSR-safe wrapper that calls `useJoyride` internally\n- `useJoyride(props)` hook — returns `{ controls, failures, on, state, step, Tour }`\n\n## State Machine\n\nTwo dimensions drive the tour:\n\n### Tour Status (overall)\n\n```mermaid\nstateDiagram-v2\n    [*] --> idle\n    idle --> ready : steps loaded\n    ready --> waiting : run=true, no steps\n    ready --> running : run=true + steps\n    waiting --> running : size > 0\n    running --> paused : stop()\n    paused --> running : start()\n    running --> finished : last step complete\n    running --> skipped : skip()\n    finished --> ready : reset()\n    skipped --> ready : reset()\n    paused --> ready : reset()\n```\n\n- `idle`: No steps loaded\n- `ready`: Steps loaded, waiting for `run=true`\n- `waiting`: `run=true` but steps loading async (transitions to RUNNING when size > 0)\n- `running`: Tour active\n- `paused`: Stopped mid-tour via `stop()` (can resume with `start()`)\n- `finished`: Tour completed normally\n- `skipped`: Tour ended via `skip()`\n\n### Step Lifecycle (per step)\n\n```mermaid\nstateDiagram-v2\n    [*] --> init\n    init --> ready : target found / before resolved\n    ready --> beacon_before : beacon path\n    ready --> tooltip_before : skip beacon\n    beacon_before --> beacon : scroll done\n    beacon --> tooltip_before : user click\n    tooltip_before --> tooltip : scroll done\n    tooltip --> complete : user action\n    complete --> [*]\n```\n\n- `init`: Step starting, target check pending, `before` hook runs here\n- `ready`: Target found and visible, preparing render\n- `beacon_before`: Beacon positioned, waiting for scroll to complete\n- `beacon`: Showing pulsing beacon\n- `tooltip_before`: Tooltip positioned, waiting for scroll to complete\n- `tooltip`: Showing tooltip content\n- `complete`: Step done, transitioning to next\n\nBeacon is skipped (goes straight to `tooltip_before`) when:\n- `skipBeacon = true`\n- `step.placement = 'center'`\n- `continuous` mode AND action is NEXT or PREV\n\nThe `*_before` phases exist to hold rendering while scroll animation completes. The scroll effect watches for `positioned=true` during these phases, scrolls, then sets `scrolling=false`, which triggers the transition to the final `beacon`/`tooltip` phase.\n\n### Complete State Transition Sequence\n\nA full multi-step tour from mount to finish:\n\n```\nmount: run=true, steps=[...]\n  → start() → status:running, lifecycle:init\n  → tour:start event\n  → [before hook runs if defined → step:before_hook event → waiting:true → loader shown]\n  → lifecycle: init→ready (target check / before hook resolved)\n  → step:before event (action: lastAction ?? update)\n  → lifecycle: ready → *_before (beacon_before or tooltip_before)\n  → scrolling:true, positioned via Floating UI → scroll animation\n  → scroll:start event\n  → scroll:end event → scrolling:false\n  → lifecycle: *_before → beacon or tooltip\n  → beacon event\n  → User clicks beacon → lifecycle: beacon → tooltip_before → tooltip\n  → tooltip event\n  → Focus trap activated\n  → User clicks \"Next\" → controls.next() → lifecycle:complete, index+1\n  → step:after event (reports previous index)\n  → [after hook runs if defined → step:after_hook event]\n  → lifecycle: complete → init (auto-advance in uncontrolled; controlled waits for parent to update stepIndex)\n  → ... repeats for each step ...\n  → Last step: index >= size → status:finished\n  → tour:end event (reports last step)\n  → controls.reset() → status:ready, index:0\n```\n\n## Controlled vs Uncontrolled\n\n**Uncontrolled** (default): Store manages index internally. `next()`/`prev()`/`go()`/`reset()` work. Auto-advances past missing targets. Supports `initialStepIndex` for starting at a specific step.\n\n**Controlled** (with `stepIndex` prop): Tour pauses at each step's COMPLETE lifecycle phase instead of auto-advancing. Parent manages the step index via `onEvent` callbacks. `go()` and `reset()` are disabled in controlled mode.\n\n## Store (`src/modules/store.ts`)\n\nClass managing immutable state via plain objects. Snapshots are `Object.freeze()`d. Created once per component instance via `useRef`. Subscriptions drive React re-renders via `useSyncExternalStore`.\n\n### State Shape\n\n```typescript\n{\n  action: Actions        // What triggered the change\n  controlled: boolean    // true if controlled prop is set\n  index: number          // Current step index\n  lifecycle: Lifecycle   // Step lifecycle phase\n  origin: Origin | null  // What triggered the action (button_close, button_skip, button_primary, keyboard, overlay)\n  positioned: boolean    // Whether floating element has been positioned (internal only)\n  scrolling: boolean     // Whether scroll animation is in progress\n  size: number           // Total steps count\n  status: Status         // Tour status\n  waiting: boolean       // Waiting for before hook or target polling\n}\n```\n\n`positioned` is internal — stripped from public `State` type via `omit(snapshot, 'positioned')`.\n\n### Store Methods\n\n| Method | Description |\n|--------|-------------|\n| `updateState(patch, forceIndex?)` | Merge patch, freeze snapshot, notify listeners. Controlled mode ignores `index` unless `forceIndex=true` |\n| `subscribe(listener)` | Returns unsubscribe function. Used by `useSyncExternalStore` |\n| `getSnapshot()` / `getServerSnapshot()` | Frozen state for `useSyncExternalStore` |\n| `getState()` | Public state (without `positioned`) |\n| `getEventState()` | Same as `getState()` — for event payloads |\n| `setPositionData(name, data)` | Stores beacon/tooltip `PositionData`. Sets `positioned=true` during `*_before` phases |\n| `getPositionData(name)` | Returns cached position data |\n| `cleanupPositionData()` | Clears both position caches (called on step init) |\n| `setSteps(steps)` | Updates internal steps array and size |\n\n`applyTransitions()` auto-promotes `WAITING → RUNNING` when `size > 0`.\n\n## Event System\n\nSingle `onEvent` prop receives `EventData` and `Controls`:\n\n```typescript\ntype EventHandler = (data: EventData, controls: Controls) => void;\n\ntype EventData = TourData & {\n  error: Error | null;      // Only for ERROR events\n  scroll: ScrollData | null; // Only for SCROLL_START/SCROLL_END\n  scrolling: boolean;\n  type: Events;\n  waiting: boolean;\n}\n```\n\nThe `controls` parameter gives event handlers direct access to tour control methods (next, prev, start, stop, etc.).\n\n```mermaid\nsequenceDiagram\n    participant Tour\n    participant Step\n    participant Scroll\n    participant UI\n\n    Tour->>Step: tour:start\n    Step->>Step: step:before_hook\n    Step->>Step: step:before\n    Step->>Scroll: scroll:start\n    Scroll-->>Step: scroll:end\n    Step->>UI: beacon\n    Note over UI: user clicks\n    UI->>UI: tooltip\n    UI->>Step: step:after\n    Step->>Step: step:after_hook\n    Note over Step: repeat per step\n    Step->>Tour: tour:end\n```\n\nAll events: `tour:start`, `step:before_hook`, `step:before`, `scroll:start`, `scroll:end`, `beacon`, `tooltip`, `step:after`, `step:after_hook`, `tour:end`, `tour:status` (on STOP/RESET), `error:target_not_found`, `error`\n\n## Controls (`src/hooks/useControls.ts`)\n\nExtracted hook providing all tour control methods. Created once via `useMemo`, returned to consumers.\n\n| Method | Effect |\n|--------|--------|\n| `close(origin?)` | RUNNING only. Sets `action:CLOSE, index+1, lifecycle:COMPLETE` |\n| `go(nextIndex)` | RUNNING + uncontrolled only. Sets `action:GO, index=nextIndex, lifecycle:COMPLETE`. FINISHED if index >= size |\n| `info()` | Returns current public state |\n| `next()` | RUNNING only. Sets `action:NEXT, index+1 (clamped to size), lifecycle:COMPLETE` |\n| `open()` | RUNNING only. Sets `lifecycle:TOOLTIP_BEFORE` to force-open tooltip |\n| `prev()` | RUNNING only. Sets `action:PREV, index-1 (clamped to 0), lifecycle:COMPLETE` |\n| `replay(origin?)` | RUNNING + lifecycle:TOOLTIP only. Sets `action:REPLAY, lifecycle:COMPLETE`. Index unchanged. Triggers full step re-entry through `step:after` → `step:before` → BEACON/TOOLTIP |\n| `reset(restart?)` | Uncontrolled only. Sets `index:0, lifecycle:INIT, status:READY (or RUNNING if restart)` |\n| `skip(origin?)` | RUNNING only. Sets `action:SKIP, lifecycle:COMPLETE, status:SKIPPED` |\n| `start(nextIndex?)` | Sets `action:START, lifecycle:INIT, status:RUNNING (or WAITING if no steps)`. Uses `forceIndex=true` |\n| `stop(advance?)` | Not if FINISHED/SKIPPED. Sets `action:STOP, lifecycle:COMPLETE, status:PAUSED`. `index+1` if advance |\n\nAll methods also reset `positioned:false, scrolling:false, waiting:false`.\n\n## Orchestration Hooks\n\n### useJoyride (`src/hooks/useJoyride.tsx`)\n\nPublic hook. Calls `useTourEngine`, wraps state (strips `positioned`), creates `TourRenderer` element, returns `{ controls, failures, on, state, step, Tour }`.\n\n### useTourEngine (`src/hooks/useTourEngine.ts`)\n\nCore orchestrator. Creates store once via `useRef(createStore(mergedProps))`, subscribes via `useSyncExternalStore`. Merges props with defaults via `useMemoDeepCompare`.\n\nOn mount: if `run=true` and steps exist and valid, calls `controls.start()`.\n\nDelegates to five sub-hooks:\n- `useControls` — tour control methods\n- `useEventEmitter` — centralized event emission\n- `usePropSync` — external prop change handling\n- `useLifecycleEffect` — state machine progression\n- `useScrollEffect` — scroll-to-target\n\n### useEventEmitter (`src/hooks/useEventEmitter.ts`)\n\nCentralizes all `onEvent` calls into a single `emitEvent(type, step, overrides?)` function. Uses refs for `onEvent` and `controls` to keep callback identity stable. Composes event data from `store.getEventState()` + defaults (`error: null`, `scroll: null`) + step + type + overrides. Passes `controls` as second argument to `onEvent`.\n\nUsed by `usePropSync`, `useLifecycleEffect`, and `useScrollEffect` instead of calling `onEvent` directly.\n\n### usePropSync (`src/hooks/usePropSync.ts`)\n\nHandles external prop changes:\n\n- **Steps change**: Validates steps, calls `store.setSteps(steps)`. Fires `error` event if invalid.\n- **Run toggle**: `run=true` → `controls.start()`, `run=false` → `controls.stop()`\n\n### useLifecycleEffect (`src/hooks/useLifecycleEffect.ts`)\n\nFive separate effects drive the state machine:\n\n**Effect 1: Action tracking** (`deps: [action]`)\nTracks NEXT/PREV/SKIP/CLOSE actions in `lastAction` ref for enriching callbacks.\n\n**Effect 2: Target resolution** (`deps: [index, lifecycle, status, store]`)\nWhen RUNNING + step exists + lifecycle is INIT:\n1. Cleans up position data\n2. If step has `before` hook: sets `waiting=true`, fires `STEP_BEFORE_HOOK`, runs the async hook with AbortController + timeout. On resolve/reject/timeout → sets `lifecycle:READY, waiting:false`\n3. If no `before` hook: checks target visibility\n   - Target visible → `lifecycle:READY`\n   - `targetWaitTimeout=0` → `lifecycle:READY` (proceed without target)\n   - Otherwise → polls at 100ms intervals, sets `waiting=true`. On timeout → `lifecycle:READY`\n\n**Effect 3: Step presentation** (`deps: [lifecycle, store]`)\nWhen lifecycle reaches READY:\n- Target visible + from INIT → fires `STEP_BEFORE` event\n- Target visible → determines final lifecycle (BEACON or TOOLTIP via `hideBeacon()`), checks if scroll needed, sets `lifecycle:*_BEFORE, scrolling:willScroll`\n- Target not visible (RUNNING, not INIT/COMPLETE) → fires `TARGET_NOT_FOUND`, auto-advances in uncontrolled mode\n\n**Effect 4: Transitions + callbacks** (`deps: [lifecycle, positioned, scrolling, store]`)\n- `*_BEFORE` phase + `scrolling=false` → transitions to final BEACON/TOOLTIP\n- BEACON lifecycle → fires `beacon` event\n- TOOLTIP lifecycle → fires `tooltip` event\n- COMPLETE lifecycle (from TOOLTIP) → fires `step:after` event, runs `after` hook (fire-and-forget), fires `step:after_hook` event\n\n**Effect 5: Tour flow** (`deps: [action, index, lifecycle, size, status, store]`)\n- Action REPLAY + COMPLETE → writes `lifecycle:INIT` (works in both controlled and uncontrolled modes; bypasses the controlled-mode pause and the FINISHED branch on the last step to allow same-index re-entry)\n- No step + lifecycle INIT → sets FINISHED\n- Uncontrolled + COMPLETE + index < size → auto-advances to INIT (controlled mode pauses here, waiting for parent to update `stepIndex`)\n- COMPLETE + index >= size → sets FINISHED\n- Status → FINISHED/SKIPPED → fires `tour:end`, calls `controls.reset()`, clears lastAction\n- Status → RUNNING (from IDLE/READY/PAUSED) → fires `tour:start`\n- Action → STOP → fires `tour:status`, clears lastAction\n- Action → RESET → fires `tour:status`, clears lastAction\n\n### useScrollEffect (`src/hooks/useScrollEffect.ts`)\n\nScrolls to target when lifecycle reaches `*_before` phases and `positioned=true` and `scrolling=true`:\n\n- Custom scroll parent: first scrolls page to bring parent into view, then scrolls within parent\n- `adjustForPlacement()`: accounts for placement (top, left, right) to avoid tooltip being off-screen after scroll\n- Fires `scroll:start` and `scroll:end` events with `ScrollData` (initial, target, element, duration)\n- Sets `scrolling:false` on completion, triggering Effect 4's `*_BEFORE → BEACON/TOOLTIP` transition\n- Cancels previous scroll if new one starts\n\n## Step Processing\n\n### Step Merging (`src/modules/step.ts`)\n\n`getMergedStep(props, currentStep)` merges in two passes:\n\n**Pass 1 — step merge:**\n```\ndefaultStep → pick(props, basePropsFields) → currentStep\n```\n\n**Pass 2 — options merge:**\n```\ndefaultOptions → props.options → pick(currentStep, optionFieldNames)\n```\n\nFinal overrides applied after merge:\n- `locale`: deep-merged `defaultLocale → props.locale → step.locale`\n- `floatingOptions`: deep-merged `defaultFloatingOptions → props.floatingOptions → step.floatingOptions`\n- `spotlightPadding`: normalized to `{ top, right, bottom, left }` via `normalizeSpotlightPadding()`\n- `styles`: generated via `getStyles(step)`\n\nSharedProps fields picked from props: `arrowComponent, beaconComponent, floatingOptions, loaderComponent, locale, styles, tooltipComponent`\n\n### Step Hooks\n\nTwo hooks per step, defined as Options (globally or per-step):\n\n**`before(data: TourData) → Promise<void>`**: Blocks the tour at INIT phase. Sets `waiting=true` immediately; shows loader after `loaderDelay` (default 300ms). Capped by `beforeTimeout` (default 5000ms). On error/timeout, fires `error` event and proceeds.\n\n**`after(data: TourData) → void`**: Fire-and-forget after STEP_AFTER. Wrapped in try-catch — user code errors don't break the tour.\n\n### Target Resolution & Tracking\n\n**Resolution** (`getElement()` in `src/modules/dom.ts`):\n- CSS selector string → `document.querySelector()`\n- HTMLElement → used directly\n- React ref → `.current`\n- Function → called, returns element or null\n\n**Visibility** (`isElementVisible()`): walks DOM tree checking `display`/`visibility`.\n\n**Position tracking** (`src/hooks/useTargetPosition.ts`):\n- ResizeObserver on target element\n- Scroll/resize event listeners (passive)\n- Returns `{ top, left, width, height, isFixed }` with spotlightPadding applied\n- Disabled during scroll/waiting to avoid jitter\n\n## Rendering Pipeline\n\n```mermaid\nflowchart TD\n    J[\"Joyride (SSR guard)\"] --> UJ[useJoyride]\n    UJ --> TR[\"TourRenderer (fragment)\"]\n    TR --> S[\"Step\"]\n    TR --> P[\"Portal element\"]\n    S --> FT[useFocusTrap]\n    S --> F[Floater]\n    F --> P\n    P --> B[\"Beacon (BEACON phases)\"]\n    P --> T[\"Tooltip (TOOLTIP phases)\"]\n    P --> A[Arrow]\n    P --> L[\"Loader (waiting=true)\"]\n    P --> O[\"Overlay (SVG spotlight)\"]\n\n    style P fill:#f9f,stroke:#333\n```\n\nAll visible UI renders through Portal to the same portal element — `TourRenderer` and `Step` produce only React fragments, not DOM elements. Floater wraps beacon/tooltip/arrow in `<Portal>`, and TourRenderer wraps overlay/loader in `<Portal>`. Both target the same element from `usePortalElement`.\n\n### Key Components\n\n**Floater** (`src/components/Floater.tsx`):\n- Wraps all rendered content (beacon or tooltip + arrow) in `<Portal>` — nothing renders inline\n- Two `useFloating()` instances: tooltip (full middleware) and beacon (offset only)\n- Tooltip middleware: `offset` (includes spotlightPadding + arrow size), `flip`/`autoPlacement`, `shift`, `arrow`, plus custom middleware from `floatingOptions`\n- Center placement: custom middleware positions at viewport center, uses virtual reference element\n- Strategy: `fixed` when `isFixed=true` or target has fixed/sticky positioning, `absolute` otherwise\n- Reports position data to store via `setPositionData()` on each calculation\n\n**Overlay** (`src/components/Overlay.tsx`):\n- SVG-based with `fillRule=\"evenodd\"` for spotlight cutout\n- `useTargetPosition` tracks target rect via ResizeObserver + scroll/resize listeners\n- Two SVG paths: overlay path (full coverage with cutout) + cover path (fades out to reveal spotlight)\n- Visible during TOOLTIP/TOOLTIP_BEFORE (continuous) or TOOLTIP only (non-continuous); also during waiting\n- Hidden during BEACON/BEACON_BEFORE phases\n\n**Beacon** (`src/components/Beacon.tsx`):\n- Pulsing animation via CSS keyframes (injected once via style tag with existence check)\n- Default: button with inner/outer animated spans\n- Custom: `beaconComponent` prop receives render props\n- Auto-focuses on mount if `shouldFocus=true`\n\n**Tooltip** (`src/components/Tooltip/index.tsx`):\n- Generates button props (back, close, primary, skip) with handlers\n- Primary button: \"Next\" (continuous + not last), \"Last\" (last step), \"Close\" (non-continuous)\n- Delegates to DefaultTooltip (default) or `tooltipComponent` (custom)\n\n**DefaultTooltip** (`src/components/Tooltip/DefaultTooltip.tsx`):\n- Default layout: title → content → footer (skip, back, primary buttons) → close button\n- Footer hidden if `buttons` array has no footer buttons (back/primary/skip)\n\n**Arrow** (`src/components/Arrow.tsx`):\n- Default: SVG arrow element positioned by Floating UI middleware\n- Custom: `arrowComponent` prop receives `ArrowRenderProps`\n\n**Loader** (`src/components/Loader.tsx`):\n- Shown when `waiting=true` (target polling or `before` hook)\n- Custom: `loaderComponent` prop. Set to `null` to disable\n\n**Portal** (`src/components/Portal.tsx`):\n- `createPortal(children, element)` wrapper\n\n**usePortalElement** (`src/hooks/usePortalElement.ts`):\n- Creates `<div id=\"react-joyride-portal\">` in body, or uses provided `portalElement` (selector or HTMLElement)\n- Cleans up auto-created div on unmount\n\n## Appendix\n\n### DOM Utilities (`src/modules/dom.ts`)\n\n- `hasPosition(el, type?)`: Recursively checks for fixed/sticky positioning\n- `getScrollParent(element)`: Uses `scrollparent` library\n- `hasCustomScrollParent(element)`: Checks if scroll parent differs from document\n- `getScrollTo(element, offset)`: Calculates scroll position\n- `getScrollTargetToCenter(element)`: Centers element in viewport\n- `scrollTo(value, options)`: Smooth scroll via `scroll` library; returns `{ cancel, promise }`\n- `scrollDocument()`: Returns document scrolling element\n- `getDocumentHeight(median?)`: Takes max or median of 5 height measurements\n- `canUseDOM()`: SSR safety check\n\n### Focus Trap (`src/hooks/useFocusTrap.ts`)\n\n- Intercepts Tab key within tooltip element\n- Wraps focus forward/backward at tabbable element boundaries\n- Focuses initial selector (`[data-action=primary]`) on mount\n- Disabled when `step.disableFocusTrap=true`\n- Restores previous focus on cleanup\n\n### Styles (`src/styles.ts`)\n\n`getStyles(step)`:\n1. Merges step styles with generated defaults\n2. Adaptive width: `min(step.width, window.innerWidth - 30)`\n3. Generates style objects (beacon, buttons, tooltip, overlay, arrow, etc.)\n4. Returns merged with user overrides\n\nSee `src/defaults.ts` for current default values.\n\n### Types (`src/types/`)\n\n| File | Description |\n|------|-------------|\n| `common.ts` | Actions, Events, Lifecycle, Origin, Status, Placement, Locale, Options, Styles |\n| `components.ts` | ArrowRenderProps, BeaconRenderProps, LoaderRenderProps, TooltipRenderProps |\n| `events.ts` | EventData, EventHandler `(data, controls)`, TourData, ScrollData |\n| `floating.ts` | FloatingOptions, PositionData |\n| `props.ts` | Props, SharedProps, UseJoyrideReturn, StepFailure |\n| `state.ts` | Controls, State |\n| `step.ts` | Step, StepMerged, StepTarget, SelectorOrElement |\n| `utilities.ts` | PartialDeep, Simplify, SetRequired, ValueOf |\n\n### Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `@floating-ui/react-dom` | Tooltip/beacon positioning (dual instances) |\n| `@fastify/deepmerge` | Deep object merging (React element aware) |\n| `@gilbarbara/hooks` | useMemoDeepCompare, useMount, usePrevious, useUpdateEffect, useWindowSize |\n| `@gilbarbara/deep-equal` | Deep equality checking (store change detection) |\n| `@gilbarbara/types` | Utility types |\n| `scroll` / `scrollparent` | Smooth scrolling and scroll parent detection |\n| `react-innertext` | Extract text from React elements (button labels) |\n| `is-lite` | Type checking utilities |\n","skills/react-joyride/references/api-props-options.md":"# Props, Options, Locale, FloatingOptions, Styles\n\n## Props\n\n```typescript\ntype Props = SharedProps & {\n  continuous?: boolean;        // Sequential play with Next button (default: false)\n  debug?: boolean;             // Log to console (default: false)\n  initialStepIndex?: number;   // Start index for uncontrolled mode (default: 0)\n  nonce?: string;              // CSP nonce for inline styles\n  onEvent?: EventHandler;      // (data: EventData, controls: Controls) => void\n  options?: Partial<Options>;  // Default options for all steps\n  portalElement?: string | HTMLElement; // Render tooltip into this element\n  run?: boolean;               // Start/stop tour (default: false)\n  scrollToFirstStep?: boolean; // Scroll to first step on start (default: false)\n  stepIndex?: number;          // Controlled mode: manages step index externally\n  steps: Array<Step>;          // Required: tour steps\n}\n```\n\n## SharedProps\n\nInherited by both `Props` and `Step`:\n\n```typescript\ntype SharedProps = {\n  arrowComponent?: ElementType<ArrowRenderProps>;\n  beaconComponent?: ElementType<BeaconRenderProps>;\n  floatingOptions?: Partial<FloatingOptions>;\n  loaderComponent?: ElementType<LoaderRenderProps> | null; // null disables loader\n  locale?: Locale;\n  styles?: PartialDeep<Styles>;\n  tooltipComponent?: ElementType<TooltipRenderProps>;\n}\n```\n\n## Options\n\nAll fields can be set globally via `options` prop or per-step (step overrides global).\n\n### Appearance\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `backgroundColor` | `string` | `'#ffffff'` | Tooltip background |\n| `primaryColor` | `string` | `'#000000'` | Primary button and beacon |\n| `textColor` | `string` | `'#000000'` | Tooltip text |\n| `overlayColor` | `string` | `'#00000080'` | Overlay backdrop |\n| `arrowColor` | `string` | `'#ffffff'` | Arrow fill |\n| `width` | `string \\| number` | `380` | Tooltip width |\n| `zIndex` | `number` | `100` | z-index for overlay/tooltip |\n\n### Arrow\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `arrowBase` | `number` | `32` | Base width in pixels |\n| `arrowSize` | `number` | `16` | Height/depth in pixels |\n| `arrowSpacing` | `number` | `12` | Distance from tooltip edge |\n\n### Beacon\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `beaconSize` | `number` | `36` | Diameter in pixels |\n| `beaconTrigger` | `'click' \\| 'hover'` | `'click'` | How to open tooltip from beacon |\n\n### Overlay & Spotlight\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `hideOverlay` | `boolean` | `false` | Don't show overlay |\n| `blockTargetInteraction` | `boolean` | `false` | Block pointer events on target |\n| `spotlightRadius` | `number` | `4` | Border radius of cutout |\n| `spotlightPadding` | `number \\| SpotlightPadding` | `10` | Padding around target |\n\n```typescript\ninterface SpotlightPadding { top?: number; right?: number; bottom?: number; left?: number; }\n```\n\n### Buttons & Interactions\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `buttons` | `ButtonType[]` | `['back','close','primary']` | Buttons in tooltip |\n| `closeButtonAction` | `'close' \\| 'skip'` | `'close'` | Close button behavior |\n| `overlayClickAction` | `'close' \\| 'next' \\| false` | `'close'` | Overlay click behavior |\n| `dismissKeyAction` | `'close' \\| 'next' \\| false` | `'close'` | ESC key behavior |\n| `showProgress` | `boolean` | `false` | Show \"N of Total\" in tooltip |\n\n`ButtonType = 'back' | 'close' | 'primary' | 'skip'`\n\n### Scroll\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `skipBeacon` | `boolean` | `false` | Skip beacon, show tooltip directly |\n| `skipScroll` | `boolean` | `false` | Don't scroll to target |\n| `scrollDuration` | `number` | `300` | Scroll animation ms |\n| `scrollOffset` | `number` | `20` | Scroll distance from element |\n| `offset` | `number` | `10` | Distance between tooltip and spotlight |\n\n### Timing & Async\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `before` | `BeforeHook` | - | `(data: TourData) => Promise<void>` — runs before step |\n| `after` | `AfterHook` | - | `(data: TourData) => void` — runs after step (fire-and-forget) |\n| `beforeTimeout` | `number` | `5000` | Max wait for before hook (0 = no timeout) |\n| `targetWaitTimeout` | `number` | `1000` | Max wait for target to appear (0 = no waiting) |\n| `loaderDelay` | `number` | `300` | Delay before showing loader |\n| `disableFocusTrap` | `boolean` | `false` | Disable focus trap on tooltip |\n\n## Locale\n\n```typescript\ninterface Locale {\n  back?: ReactNode;              // 'Back'\n  close?: ReactNode;             // 'Close'\n  last?: ReactNode;              // 'Last'\n  next?: ReactNode;              // 'Next'\n  nextWithProgress?: ReactNode;  // 'Next ({current} of {total})'\n  open?: ReactNode;              // 'Open the dialog'\n  skip?: ReactNode;              // 'Skip'\n}\n```\n\n`{current}` and `{total}` are placeholder strings replaced at render time in `nextWithProgress`.\n\n## FloatingOptions\n\nControls `@floating-ui/react-dom` positioning:\n\n```typescript\ninterface FloatingOptions {\n  autoUpdate?: Partial<AutoUpdateOptions>;     // Auto-reposition options\n  beaconOptions?: { offset?: number };         // Beacon offset (default: -18)\n  flipOptions?: Partial<FlipOptions> | false;  // Flip middleware (false to disable)\n  hideArrow?: boolean;                         // Hide arrow (default: false)\n  middleware?: Array<Middleware>;               // Additional Floating UI middleware\n  onPosition?: (data: PositionData) => void;   // Position callback\n  shiftOptions?: Partial<ShiftOptions>;        // Shift middleware options\n  strategy?: 'fixed' | 'absolute';             // Auto-detected from step.isFixed\n}\n```\n\nDocs: https://v3.react-joyride.com/docs/props/floating-options\n\n## Styles\n\nOverride CSS for any UI element:\n\n```typescript\ninterface Styles {\n  arrow: CSSProperties;\n  beacon: CSSProperties;\n  beaconInner: CSSProperties;\n  beaconOuter: CSSProperties;\n  beaconWrapper: CSSProperties;\n  buttonBack: CSSProperties;\n  buttonClose: CSSProperties;\n  buttonPrimary: CSSProperties;\n  buttonSkip: CSSProperties;\n  floater: CSSProperties;\n  loader: CSSProperties;\n  overlay: CSSProperties;\n  tooltip: CSSProperties;\n  tooltipContainer: CSSProperties;\n  tooltipContent: CSSProperties;\n  tooltipFooter: CSSProperties;\n  tooltipFooterSpacer: CSSProperties;\n  tooltipTitle: CSSProperties;\n}\n```\n\nUse `PartialDeep<Styles>` — only override what you need.\n\nDocs: https://v3.react-joyride.com/docs/props/styles\n","skills/react-joyride/references/patterns.md":"# Common Patterns\n\n## Async Transitions with `before`/`after` Hooks (Preferred)\n\nThe library handles waiting for async operations. Use `before` hooks to prepare the UI before a step shows (open dropdowns, load data, trigger animations). The tour waits for the promise to resolve. Use `after` hooks for cleanup (fire-and-forget, does not block).\n\nThis is the **preferred approach** over controlled mode for most async scenarios.\n\n```tsx\nimport { useState } from 'react';\nimport { ACTIONS, STATUS, useJoyride } from 'react-joyride';\n\nfunction Dashboard() {\n  const [run, setRun] = useState(false);\n  const [isMenuOpen, setIsMenuOpen] = useState(false);\n\n  const { Tour } = useJoyride({\n    continuous: true,\n    run,\n    steps: [\n      {\n        target: '.nav-bar',\n        content: 'This is the navigation',\n        skipBeacon: true,\n      },\n      {\n        target: '.dropdown-item', // Element inside the dropdown\n        content: 'This dropdown item is important',\n        skipBeacon: true,\n        before: ({ action }) => {\n          // Open dropdown before this step — runs on both forward and back navigation\n          setIsMenuOpen(true);\n          return new Promise(resolve => setTimeout(resolve, 300));\n        },\n      },\n      {\n        target: '.main-content',\n        content: 'And here is the main content',\n        before: ({ action }) => {\n          // Close dropdown when moving to this step\n          setIsMenuOpen(false);\n          return new Promise(resolve => setTimeout(resolve, 300));\n        },\n      },\n    ],\n    onEvent: (data) => {\n      if ([STATUS.FINISHED, STATUS.SKIPPED].includes(data.status)) {\n        setRun(false);\n        setIsMenuOpen(false);\n      }\n    },\n  });\n\n  return (\n    <div>\n      {Tour}\n      <button onClick={() => setRun(true)}>Start Tour</button>\n      {/* ... */}\n    </div>\n  );\n}\n```\n\n## Before/After Hooks\n\n### Async data loading before a step\n\n```tsx\n{\n  target: '.user-profile',\n  content: 'Here is your profile data',\n  before: async () => {\n    await fetchUserProfile();\n    // Tour waits for this promise to resolve\n  },\n  beforeTimeout: 10000, // Allow up to 10s\n}\n```\n\n### Analytics tracking after a step\n\n```tsx\n{\n  target: '.feature',\n  content: 'Check out this feature',\n  after: (data) => {\n    // Fire-and-forget — does not block the tour\n    analytics.track('tour_step_completed', {\n      stepIndex: data.index,\n      action: data.action,\n    });\n  },\n}\n```\n\n### Delayed transitions (e.g., waiting for animation)\n\n```tsx\n{\n  target: '.sidebar',\n  content: 'The sidebar',\n  before: ({ action }) => {\n    // Only delay when navigating forward (not on back)\n    const ms = action === ACTIONS.PREV ? 0 : 300;\n    return new Promise(resolve => setTimeout(resolve, ms));\n  },\n}\n```\n\n## Custom Tooltip Component\n\n```tsx\nimport type { TooltipRenderProps } from 'react-joyride';\n\nfunction CustomTooltip({\n  backProps,\n  closeProps,\n  index,\n  isLastStep,\n  primaryProps,\n  size,\n  skipProps,\n  step,\n  tooltipProps,\n}: TooltipRenderProps) {\n  return (\n    <div\n      {...tooltipProps}\n      style={{\n        background: '#fff',\n        borderRadius: 8,\n        maxWidth: 400,\n        padding: 20,\n        width: step.width,\n      }}\n    >\n      {step.title && <h3 style={{ margin: '0 0 8px' }}>{step.title}</h3>}\n      <div>{step.content}</div>\n\n      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 16 }}>\n        {step.buttons.includes('skip') && !isLastStep && (\n          <button {...skipProps} type=\"button\">Skip</button>\n        )}\n        <div style={{ display: 'flex', gap: 8, marginLeft: 'auto' }}>\n          {index > 0 && (\n            <button {...backProps} type=\"button\">Back</button>\n          )}\n          <button {...primaryProps} type=\"button\">\n            {isLastStep ? 'Done' : `Next (${index + 1}/${size})`}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n// Usage:\n// <Joyride tooltipComponent={CustomTooltip} ... />\n// or in useJoyride: useJoyride({ tooltipComponent: CustomTooltip, ... })\n```\n\n## Event Subscription with `on()`\n\n```tsx\nfunction App() {\n  const { on, Tour } = useJoyride({\n    steps,\n    run: true,\n    continuous: true,\n  });\n\n  useEffect(() => {\n    const unsubs = [\n      on('tour:start', (data) => {\n        console.log('Tour started');\n      }),\n      on('tooltip', (data) => {\n        analytics.track('step_viewed', { index: data.index });\n      }),\n      on('tour:end', (data) => {\n        const wasSkipped = data.status === 'skipped';\n        analytics.track('tour_ended', { skipped: wasSkipped, lastStep: data.index });\n      }),\n    ];\n\n    return () => unsubs.forEach(unsub => unsub());\n  }, [on]);\n\n  return <div>{Tour}</div>;\n}\n```\n\n## Dynamic Steps\n\n```tsx\nfunction App() {\n  const [steps, setSteps] = useState<Step[]>([]);\n\n  useEffect(() => {\n    // Build steps based on feature flags, user role, etc.\n    const dynamicSteps: Step[] = [\n      { target: '.dashboard', content: 'Welcome to your dashboard' },\n    ];\n\n    if (user.isAdmin) {\n      dynamicSteps.push({\n        target: '.admin-panel',\n        content: 'Admin controls are here',\n      });\n    }\n\n    if (featureFlags.newSearch) {\n      dynamicSteps.push({\n        target: '.search-bar',\n        content: 'Try the new search',\n      });\n    }\n\n    setSteps(dynamicSteps);\n  }, [user, featureFlags]);\n\n  return (\n    <Joyride\n      run={steps.length > 0}\n      steps={steps}\n      continuous\n    />\n  );\n}\n```\n\n## React Ref Targets\n\n```tsx\nfunction App() {\n  const sidebarRef = useRef<HTMLDivElement>(null);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n\n  const steps: Step[] = [\n    { target: sidebarRef, content: 'Navigation sidebar' },\n    { target: buttonRef, content: 'Click here to create' },\n    { target: '.css-selector', content: 'Mix refs with selectors' },\n  ];\n\n  return (\n    <div>\n      <Joyride steps={steps} run continuous />\n      <div ref={sidebarRef}>Sidebar</div>\n      <button ref={buttonRef}>Create</button>\n      <span className=\"css-selector\">Other element</span>\n    </div>\n  );\n}\n```\n\n## Restart / Resume Tour\n\n```tsx\nfunction App() {\n  const [run, setRun] = useState(false);\n  const [initialStepIndex, setInitialStepIndex] = useState(0);\n\n  const handleStart = () => {\n    setInitialStepIndex(0);\n    setRun(true);\n  };\n\n  const handleResume = (fromStep: number) => {\n    setInitialStepIndex(fromStep);\n    setRun(true);\n  };\n\n  return (\n    <div>\n      <Joyride\n        run={run}\n        initialStepIndex={initialStepIndex}\n        steps={steps}\n        continuous\n        onEvent={(data) => {\n          if ([STATUS.FINISHED, STATUS.SKIPPED].includes(data.status)) {\n            setRun(false);\n          }\n        }}\n      />\n      <button onClick={handleStart}>Start Tour</button>\n      <button onClick={() => handleResume(3)}>Resume from Step 4</button>\n    </div>\n  );\n}\n```\n\n## Center Placement (Modal Style)\n\n```tsx\n{\n  target: 'body',\n  placement: 'center',\n  content: (\n    <div>\n      <h2>Welcome!</h2>\n      <p>This appears as a centered modal overlay.</p>\n    </div>\n  ),\n  // Center placement automatically hides beacon and arrow\n}\n```\n","skills/react-joyride/SKILL.md":"---\nname: react-joyride\ndescription: >-\n  Guide for implementing, configuring, and debugging React Joyride v3 guided tours.\n  Use this skill whenever the user mentions joyride, guided tour, onboarding tour,\n  walkthrough, tooltip tour, step-by-step guide, product tour, or wants to highlight\n  UI elements sequentially. Also use when debugging tour issues like tooltips not\n  appearing, targets not found, or controlled mode problems. This skill covers the\n  useJoyride hook, Joyride component, step configuration, events, controls, custom\n  components, and styling.\nlicense: MIT\nmetadata:\n  author: gilbarbara\n  version: \"3.0.0\"\n---\n\n# React Joyride v3\n\nCreate guided tours in React apps. Two public APIs: the `useJoyride()` hook (recommended) and the `<Joyride>` component.\n\nOnline docs: https://v3.react-joyride.com\n\n## Quick Start\n\n### Using the hook (recommended)\n\n```tsx\nimport { useJoyride, STATUS, Status } from 'react-joyride';\n\nfunction App() {\n  const { Tour } = useJoyride({\n    continuous: true,\n    run: true,\n    steps: [\n      { target: '.my-element', content: 'This is the first step', title: 'Welcome' },\n      { target: '#sidebar', content: 'Navigate here', placement: 'right' },\n    ],\n    onEvent: (data) => {\n      if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n        // Tour ended\n      }\n    },\n  });\n\n  return <div>{Tour}{/* rest of app */}</div>;\n}\n```\n\n### Using the component\n\n```tsx\nimport { Joyride, STATUS, Status } from 'react-joyride';\n\nfunction App() {\n  return (\n    <Joyride\n      continuous\n      run={true}\n      steps={[\n        { target: '.my-element', content: 'First step' },\n        { target: '#sidebar', content: 'Second step' },\n      ]}\n      onEvent={(data) => {\n        if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n          // Tour ended\n        }\n      }}\n    />\n  );\n}\n```\n\nThe hook returns `{ controls, failures, on, state, step, Tour }`. Render `Tour` in your JSX.\n\nDocs: https://v3.react-joyride.com/docs/getting-started\n\n## Core Concepts\n\nThe tour has two state dimensions:\n\n**Tour Status**: `idle -> ready -> waiting -> running <-> paused -> finished | skipped`\n- `idle`: No steps loaded\n- `ready`: Steps loaded, waiting for `run: true`\n- `waiting`: `run=true` but steps loading async (transitions to running when steps arrive)\n- `running`: Tour active\n- `paused`: Tour paused (controlled mode at COMPLETE, or `stop()` called)\n- `finished` / `skipped`: Tour ended\n\n**Step Lifecycle** (per step): `init -> ready -> beacon_before -> beacon -> tooltip_before -> tooltip -> complete`\n- `*_before` phases: scrolling and positioning happen here\n- `beacon`: Pulsing indicator shown (skipped when `continuous` + navigating, `skipBeacon`, or `placement: 'center'`)\n- `tooltip`: The tooltip is visible and interactive\n\nDocs: https://v3.react-joyride.com/docs/how-it-works\n\n## Step Configuration\n\nEach step requires `target` and `content`. All other fields are optional.\n\n```tsx\n{\n  target: '.my-element',       // CSS selector, HTMLElement, React ref, or () => HTMLElement\n  content: 'Step body text',   // ReactNode\n  title: 'Optional title',    // ReactNode\n  placement: 'bottom',        // Default. Also: top, left, right, *-start, *-end, auto, center\n  id: 'unique-id',            // Optional identifier\n  data: { custom: 'data' },   // Attached to event callbacks\n}\n```\n\n### Target types\n\n```tsx\n// CSS selector\n{ target: '.sidebar-nav' }\n// HTMLElement\n{ target: document.getElementById('my-el') }\n// React ref\nconst ref = useRef(null);\n{ target: ref }\n// Function (evaluated each lifecycle)\n{ target: () => document.querySelector('.dynamic-element') }\n```\n\n### Common step options (override per-step)\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `placement` | `'bottom'` | Tooltip position. Use `'center'` for modal-style (requires `target: 'body'`) |\n| `skipBeacon` | `false` | Skip beacon, show tooltip directly |\n| `buttons` | `['back','close','primary']` | Buttons in tooltip. Add `'skip'` for skip button |\n| `hideOverlay` | `false` | Don't show dark overlay |\n| `blockTargetInteraction` | `false` | Block clicks on highlighted element |\n| `before` | - | `(data) => Promise<void>` — async hook before step shows |\n| `after` | - | `(data) => void` — fire-and-forget hook after step completes |\n| `skipScroll` | `false` | Don't scroll to target |\n| `scrollTarget` | - | Scroll to this element instead of `target` |\n| `spotlightTarget` | - | Highlight this element instead of `target` |\n| `spotlightPadding` | `10` | Padding around spotlight. Number or `{ top, right, bottom, left }` |\n| `targetWaitTimeout` | `1000` | ms to wait for target to appear. `0` = no waiting |\n| `beforeTimeout` | `5000` | ms to wait for `before` hook. `0` = no timeout |\n\nAll `Options` fields can be set globally via `options` prop or per-step. Per-step values override global.\n\nDocs: https://v3.react-joyride.com/docs/step | https://v3.react-joyride.com/docs/props/options\n\n## Uncontrolled vs Controlled\n\n### Uncontrolled (default — strongly preferred)\n\nThe tour manages step navigation internally. This is the right choice for most use cases.\n\n**The library handles async transitions for you.** If a step needs to wait for a UI change (dropdown opening, data loading, animation), use `before` hooks — the tour waits for the promise to resolve before showing the step. If a target element isn't in the DOM yet, `targetWaitTimeout` (default: 1000ms) handles polling for it. You do NOT need controlled mode for these cases.\n\n```tsx\nconst { Tour } = useJoyride({\n  continuous: true,\n  run: isRunning,\n  steps: [\n    { target: '.nav', content: 'Navigation' },\n    {\n      target: '.dropdown-item',\n      content: 'Inside the dropdown',\n      before: () => {\n        // Open dropdown and wait for animation — tour waits automatically\n        openDropdown();\n        return new Promise(resolve => setTimeout(resolve, 300));\n      },\n      after: () => closeDropdown(), // Clean up after step (fire-and-forget)\n    },\n    { target: '.main-content', content: 'Main content' },\n  ],\n  onEvent: (data) => {\n    if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n      setIsRunning(false);\n    }\n  },\n});\n```\n\n### Controlled (with `stepIndex`) — use sparingly\n\nOnly use controlled mode when the parent genuinely needs to manage the step index externally (e.g., syncing with URL params, external state machines, or complex multi-component coordination that `before`/`after` hooks can't handle).\n\n```tsx\nconst [stepIndex, setStepIndex] = useState(0);\nconst [run, setRun] = useState(true);\n\nconst { Tour } = useJoyride({\n  continuous: true,\n  run,\n  stepIndex,  // This makes it controlled\n  steps,\n  onEvent: (data) => {\n    const { action, index, status, type } = data;\n\n    if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(status)) {\n      setRun(false);\n      return;\n    }\n\n    if (type === 'step:after' || type === 'error:target_not_found') {\n      setStepIndex(index + (action === 'prev' ? -1 : 1));\n    }\n  },\n});\n```\n\n**Controlled mode rules:**\n- `go()` and `reset()` are disabled (logged warning)\n- You must update `stepIndex` in response to events\n- The tour pauses at COMPLETE — you must advance it\n- Prefer uncontrolled mode with `before`/`after` hooks unless you have a strong reason for external index management\n\n## Event System\n\n### `onEvent` callback\n\n```tsx\nonEvent: (data: EventData, controls: Controls) => void\n```\n\nThe `data` object contains the full tour state plus event-specific fields. The `controls` object lets you programmatically control the tour.\n\n### Event types (in order per step)\n\n| Event | When |\n|-------|------|\n| `tour:start` | Tour begins |\n| `step:before_hook` | `before` hook is called |\n| `step:before` | Target found, step about to render |\n| `scroll:start` | Scrolling to target |\n| `scroll:end` | Scroll complete |\n| `beacon` | Beacon shown |\n| `tooltip` | Tooltip shown |\n| `step:after` | User navigated (next/prev/close/skip) |\n| `step:after_hook` | `after` hook called |\n| `tour:end` | Tour finished or skipped |\n| `tour:status` | Status changed (on stop/reset) |\n| `error:target_not_found` | Target element not found |\n| `error` | Generic error |\n\n### Event subscription with `on()`\n\n```tsx\nconst { on, Tour } = useJoyride({ ... });\n\nuseEffect(() => {\n  const unsubscribe = on('tooltip', (data, controls) => {\n    analytics.track('tour_step_viewed', { step: data.index });\n  });\n  return unsubscribe;\n}, [on]);\n```\n\nDocs: https://v3.react-joyride.com/docs/events\n\n## Controls\n\nAvailable via `useJoyride()` return value or `onEvent` second argument:\n\n| Method | Description |\n|--------|-------------|\n| `next()` | Advance to next step |\n| `prev()` | Go to previous step |\n| `close(origin?)` | Close current step, advance |\n| `skip(origin?)` | Skip the tour entirely |\n| `start(index?)` | Start the tour |\n| `stop(advance?)` | Stop (pause) the tour |\n| `go(index)` | Jump to step (uncontrolled only) |\n| `reset(restart?)` | Reset tour (uncontrolled only) |\n| `open()` | Open tooltip for current step |\n| `info()` | Get current state |\n\nDocs: https://v3.react-joyride.com/docs/hook\n\n## Styling & Theming\n\nThree layers of customization (from simple to full control):\n\n### 1. Color options (simplest)\n\n```tsx\noptions: {\n  primaryColor: '#1976d2',      // Buttons and beacon\n  backgroundColor: '#1a1a2e',   // Tooltip background\n  textColor: '#ffffff',         // Tooltip text\n  overlayColor: 'rgba(0,0,0,0.7)', // Overlay backdrop\n  arrowColor: '#1a1a2e',        // Arrow (match background)\n}\n```\n\n### 2. Styles override\n\n```tsx\nstyles: {\n  tooltip: { borderRadius: 12 },\n  buttonPrimary: { backgroundColor: '#1976d2' },\n  buttonBack: { color: '#666' },\n  spotlight: { borderRadius: 8 },\n}\n```\n\nStyle keys: `arrow`, `beacon`, `beaconInner`, `beaconOuter`, `beaconWrapper`, `buttonBack`, `buttonClose`, `buttonPrimary`, `buttonSkip`, `floater`, `loader`, `overlay`, `tooltip`, `tooltipContainer`, `tooltipContent`, `tooltipFooter`, `tooltipFooterSpacer`, `tooltipTitle`\n\n### 3. Custom components (full control)\n\nSee next section.\n\nDocs: https://v3.react-joyride.com/docs/props/styles\n\n## Custom Components\n\nReplace any UI component via props. Each receives render props with step data and button handlers.\n\n### Custom Tooltip\n\n```tsx\nimport type { TooltipRenderProps } from 'react-joyride';\n\nfunction MyTooltip({ backProps, index, primaryProps, size, skipProps, step, tooltipProps }: TooltipRenderProps) {\n  return (\n    <div {...tooltipProps} style={{ background: '#fff', padding: 16, borderRadius: 8, width: step.width }}>\n      {step.title && <h3>{step.title}</h3>}\n      <div>{step.content}</div>\n      <div>\n        {index > 0 && <button {...backProps}>Back</button>}\n        <button {...primaryProps}>Next</button>\n      </div>\n    </div>\n  );\n}\n\n// Usage\n<Joyride tooltipComponent={MyTooltip} ... />\n```\n\n**Important**: Spread `tooltipProps` on the container (sets `role=\"dialog\"` and `aria-modal`). Spread button props (`backProps`, `primaryProps`, `closeProps`, `skipProps`) on buttons for correct action handling.\n\n### Custom Beacon\n\nMust render a `<span>` (placed inside a `<button>` wrapper). Receives `BeaconRenderProps`: `{ continuous, index, isLastStep, size, step }`.\n\n### Custom Arrow\n\nReceives `ArrowRenderProps`: `{ base, placement, size }`.\n\n### Custom Loader\n\nReceives `LoaderRenderProps`: `{ step }`. Set to `null` to disable the loader.\n\nDocs: https://v3.react-joyride.com/docs/custom-components\n\n## Problem → Solution Guide\n\n| I need to... | Use |\n|---|---|\n| Wait for async UI changes between steps (dropdown, animation, data load) | `before` hook returning a Promise — not controlled mode |\n| Control step navigation externally (URL sync, external state machine) | Controlled mode with `stepIndex` — but try `before`/`after` hooks first |\n| Track which steps failed (target missing, hook error) | `failures` array from `useJoyride()` return |\n| Listen to specific events without `onEvent` switch | `on('event:type', handler)` from `useJoyride()` return |\n| Show a centered modal-style step | `target: 'body'` + `placement: 'center'` |\n\n## Common Gotchas & Debugging\n\n### Use `debug: true` first\n\nThe `debug` prop is the most powerful troubleshooting tool. It logs detailed lifecycle transitions, state changes, and event emissions to the console. Always start here when something isn't working.\n\n```tsx\n<Joyride debug={true} ... />\n// or\nuseJoyride({ debug: true, ... })\n```\n\nThe console output shows exactly which lifecycle phase the tour reaches, what actions are firing, and where it gets stuck.\n\n### Tour not starting\n- Check `run={true}` is set\n- Verify `steps` array is not empty and steps have valid `target` + `content`\n- For SSR: use `<Joyride>` component (auto-guards DOM access) or check `typeof window !== 'undefined'`\n\n### Target not found\n- Test selector in console: `document.querySelector('.your-selector')`\n- Element must be visible (not `display: none`, `visibility: hidden`, or zero dimensions)\n- If element mounts later, increase `targetWaitTimeout` (default: 1000ms)\n- Set `targetWaitTimeout: 0` to skip waiting entirely\n- In uncontrolled mode, missing targets auto-advance; in controlled mode, handle `error:target_not_found` event\n\n### Tooltip never appears / overlay flashes\n- Add `debug: true` and check the console to see which lifecycle phase is reached\n- Verify the target element is in the viewport or scrollable\n- Check for CSS `overflow: hidden` on ancestors clipping the target\n- If using portals or modals, the target may not be accessible\n\n### Controlled mode stuck\n- First question: do you actually need controlled mode? Most async needs are solved with `before`/`after` hooks in uncontrolled mode\n- If controlled: you MUST update `stepIndex` in your `onEvent` handler when `type === 'step:after'`\n- Handle both forward (`action !== 'prev'`) and backward (`action === 'prev'`) navigation\n- Also handle `error:target_not_found` to skip broken steps\n- `go()` and `reset()` don't work in controlled mode\n\n### Before hook timeout\n- Default `beforeTimeout` is 5000ms — increase if your async operation takes longer\n- Set `beforeTimeout: 0` for no timeout\n- The loader appears after `loaderDelay` (300ms) while waiting\n\n### Scroll issues\n- Use `scrollTarget` to scroll to a different element than the tooltip target\n- Adjust `scrollOffset` (default: 20px) for headers or fixed elements\n- Set `skipScroll: true` to disable auto-scrolling for a step\n- `scrollToFirstStep: false` by default — set to `true` if first step is off-screen\n\n### Center placement\n- Use `placement: 'center'` with `target: 'body'` for modal-style centered tooltips\n- Center placement automatically hides the beacon and arrow\n\n### Imports\n\n```tsx\n// Named exports only (no default export in v3)\nimport { Joyride, useJoyride } from 'react-joyride';\n\n// Constants for type-safe comparisons\nimport { ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS } from 'react-joyride';\n\n// Types\nimport type { Step, Props, EventData, Controls, TooltipRenderProps } from 'react-joyride';\n```\n\nDocs: https://v3.react-joyride.com/docs/exports\n\n## Reference Files\n\nRead these for complete API details:\n\n- `references/api-props-options.md` — Full Props, Options (all 30+ fields with defaults), Locale, FloatingOptions, Styles types\n- `references/api-step-state-controls.md` — Step, StepMerged, StepTarget, State, Controls (all 10 methods), UseJoyrideReturn, StepFailure\n- `references/api-events-components.md` — All 13 event types, ACTIONS/LIFECYCLE/STATUS/ORIGIN constants, EventData, custom component render props\n- `references/patterns.md` — Complete working examples: controlled mode, before/after hooks, custom tooltip, event subscription, dynamic steps\n"},"files":{"skills/react-joyride/references/api-step-state-controls.md":"# Step, State, Controls\n\n## Step\n\nA single step provided by the user. Extends `SharedProps` and `Partial<Options>`.\n\n```typescript\ntype Step = SharedProps & Partial<Options> & {\n  // Required\n  content: ReactNode;                           // Tooltip body\n  target: StepTarget;                           // Element to highlight\n\n  // Optional\n  id?: string;                                  // Unique identifier\n  title?: ReactNode;                            // Tooltip title\n  data?: any;                                   // Custom data (passed to callbacks)\n\n  // Positioning\n  placement?: Placement | 'auto' | 'center';   // Default: 'bottom'\n  beaconPlacement?: Placement;                  // Beacon-specific placement\n  isFixed?: boolean;                            // Force fixed positioning (default: false)\n\n  // Alternate targets\n  scrollTarget?: StepTarget;                    // Scroll to this instead of target\n  spotlightTarget?: StepTarget;                 // Highlight this instead of target\n}\n```\n\nAll `Options` fields (see api-props-options.md) are also valid on Step for per-step overrides.\n\n## StepTarget\n\nFour ways to specify a target element:\n\n```typescript\ntype StepTarget =\n  | string                              // CSS selector\n  | HTMLElement                         // Direct element reference\n  | RefObject<HTMLElement | null>       // React ref\n  | (() => HTMLElement | null);         // Function (re-evaluated each lifecycle)\n```\n\n## StepMerged\n\nThe normalized step after defaults are applied. All `Options` fields become required, plus:\n- `spotlightPadding: Required<SpotlightPadding>` (always `{ top, right, bottom, left }`)\n- `styles: Styles` (fully resolved)\n\nThis is what you receive in event callbacks and custom component props.\n\n## State\n\n```typescript\ntype State = {\n  action: Actions;         // What triggered the update (init, start, next, prev, etc.)\n  controlled: boolean;     // true when stepIndex prop is set\n  index: number;           // Current step index\n  lifecycle: Lifecycle;     // Step rendering phase\n  origin: Origin | null;   // UI element that triggered action\n  scrolling: boolean;      // Scroll animation in progress\n  size: number;            // Total number of steps\n  status: Status;          // Tour status (idle, ready, running, etc.)\n  waiting: boolean;        // Blocked on before hook or target polling\n}\n```\n\n## Controls\n\nAll 10 methods for programmatic tour control:\n\n```typescript\ntype Controls = {\n  close(origin?: Origin | null): void;\n  // Close the current step and advance to the next one.\n  // Optional origin for event tracking.\n\n  go(nextIndex: number): void;\n  // Jump to a specific step by index.\n  // Uncontrolled mode only — logs warning in controlled mode.\n\n  info(): State;\n  // Get the current tour state snapshot.\n\n  next(): void;\n  // Advance to the next step.\n\n  open(): void;\n  // Open the tooltip for the current step (skip beacon).\n\n  prev(): void;\n  // Go back to the previous step.\n\n  reset(restart?: boolean): void;\n  // Reset the tour to the beginning.\n  // If restart=true, also starts the tour.\n  // Uncontrolled mode only — logs warning in controlled mode.\n\n  skip(origin?: 'button_close' | 'button_skip'): void;\n  // Skip (end) the tour entirely. Sets status to SKIPPED.\n\n  start(nextIndex?: number): void;\n  // Start the tour. Optionally at a specific step index.\n\n  stop(advance?: boolean): void;\n  // Stop (pause) the tour. Sets status to PAUSED.\n  // If advance=true, moves to next step before stopping.\n}\n```\n\n## UseJoyrideReturn\n\n```typescript\ntype UseJoyrideReturn = {\n  controls: Controls;\n  // Programmatic tour control methods.\n\n  failures: StepFailure[];\n  // Steps that failed (target not found, before hook errors).\n  // Clears on start/reset.\n\n  on: (eventType: Events, handler: EventHandler) => () => void;\n  // Subscribe to a specific event type.\n  // Returns an unsubscribe function.\n\n  state: State;\n  // Current tour state (reactive, updates on changes).\n\n  step: StepMerged | null;\n  // Current merged step, or null if no step is active.\n\n  Tour: ReactElement | null;\n  // The tour React element. Render this in your JSX.\n}\n```\n\n## StepFailure\n\n```typescript\ninterface StepFailure {\n  reason: 'before_hook' | 'target_not_found';\n  step: StepMerged;\n}\n```\n\n## Placement\n\n```typescript\ntype Placement =\n  | 'top' | 'top-start' | 'top-end'\n  | 'bottom' | 'bottom-start' | 'bottom-end'\n  | 'left' | 'left-start' | 'left-end'\n  | 'right' | 'right-start' | 'right-end';\n\n// Step also accepts: 'auto' (Floating UI auto-placement) and 'center' (modal-style)\n```\n","skills/react-joyride/references/api-events-components.md":"# Events, Constants, Custom Components\n\n## Constants\n\n### ACTIONS\n```typescript\nconst ACTIONS = {\n  INIT: 'init',\n  START: 'start',\n  STOP: 'stop',\n  RESET: 'reset',\n  PREV: 'prev',\n  NEXT: 'next',\n  GO: 'go',\n  CLOSE: 'close',\n  SKIP: 'skip',\n  UPDATE: 'update',\n  COMPLETE: 'complete',\n} as const;\n```\n\n### EVENTS\n```typescript\nconst EVENTS = {\n  TOUR_START: 'tour:start',\n  STEP_BEFORE_HOOK: 'step:before_hook',\n  STEP_BEFORE: 'step:before',\n  SCROLL_START: 'scroll:start',\n  SCROLL_END: 'scroll:end',\n  BEACON: 'beacon',\n  TOOLTIP: 'tooltip',\n  STEP_AFTER: 'step:after',\n  STEP_AFTER_HOOK: 'step:after_hook',\n  TOUR_END: 'tour:end',\n  TOUR_STATUS: 'tour:status',\n  TARGET_NOT_FOUND: 'error:target_not_found',\n  ERROR: 'error',\n} as const;\n```\n\n### LIFECYCLE\n```typescript\nconst LIFECYCLE = {\n  INIT: 'init',\n  READY: 'ready',\n  BEACON_BEFORE: 'beacon_before',\n  BEACON: 'beacon',\n  TOOLTIP_BEFORE: 'tooltip_before',\n  TOOLTIP: 'tooltip',\n  COMPLETE: 'complete',\n} as const;\n```\n\n### STATUS\n```typescript\nconst STATUS = {\n  IDLE: 'idle',\n  READY: 'ready',\n  WAITING: 'waiting',\n  RUNNING: 'running',\n  PAUSED: 'paused',\n  SKIPPED: 'skipped',\n  FINISHED: 'finished',\n} as const;\n```\n\n### ORIGIN\n```typescript\nconst ORIGIN = {\n  BUTTON_CLOSE: 'button_close',\n  BUTTON_SKIP: 'button_skip',\n  BUTTON_PRIMARY: 'button_primary',\n  KEYBOARD: 'keyboard',\n  OVERLAY: 'overlay',\n} as const;\n```\n\n## Event Types\n\n### EventData\n\nThe full payload passed to `onEvent` and event subscribers:\n\n```typescript\ntype EventData = TourData & {\n  error: Error | null;         // Only for 'error' events\n  scroll: ScrollData | null;   // Only for 'scroll:start' / 'scroll:end'\n  scrolling: boolean;\n  type: Events;                // The event type (discriminator)\n  waiting: boolean;\n}\n```\n\n### TourData\n\nBase payload (also passed to `before` and `after` hooks):\n\n```typescript\ninterface TourData {\n  action: Actions;\n  controlled: boolean;\n  index: number;\n  lifecycle: Lifecycle;\n  origin: Origin | null;\n  size: number;\n  status: Status;\n  step: StepMerged;\n}\n```\n\n### EventHandler\n\n```typescript\ntype EventHandler = (data: EventData, controls: Controls) => void;\n```\n\n### ScrollData\n\n```typescript\ntype ScrollData = {\n  duration: number;     // Scroll duration in ms\n  element: Element;     // Element being scrolled\n  initial: number;      // Scroll position before\n  target: number;       // Computed scroll destination\n}\n```\n\n## Event Flow (per step)\n\n```\ntour:start (once)\n  |\n  v\nstep:before_hook  (if before hook exists)\n  |\n  v\nstep:before  (target found, step about to render)\n  |\n  v\nscroll:start / scroll:end  (if scrolling needed)\n  |\n  v\nbeacon  (if beacon shown) OR tooltip  (if beacon skipped)\n  |\n  v  (user clicks beacon)\ntooltip\n  |\n  v  (user clicks next/prev/close/skip)\nstep:after\n  |\n  v\nstep:after_hook  (if after hook exists)\n  |\n  v  (next step or tour end)\ntour:end  (when finished/skipped)\n```\n\n## Custom Component Render Props\n\n### TooltipRenderProps\n\n```typescript\ntype TooltipRenderProps = {\n  // Tour state\n  continuous: boolean;\n  index: number;\n  isLastStep: boolean;\n  size: number;\n  step: StepMerged;\n\n  // Button props (spread on button elements)\n  backProps: {\n    'aria-label': string;\n    'data-action': string;\n    onClick: MouseEventHandler<HTMLElement>;\n    role: string;\n    title: string;\n  };\n  closeProps: { /* same shape */ };\n  primaryProps: { /* same shape */ };\n  skipProps: { /* same shape */ };\n\n  // Container props (spread on tooltip wrapper)\n  tooltipProps: {\n    'aria-modal': boolean;\n    role: string;  // 'dialog'\n  };\n\n  // Tour controls\n  controls: Controls;\n}\n```\n\n**Important**: Always spread `tooltipProps` on the root element and button props on their respective buttons. This ensures correct accessibility attributes and action handling.\n\n### BeaconRenderProps\n\n```typescript\ntype BeaconRenderProps = {\n  continuous: boolean;\n  index: number;\n  isLastStep: boolean;\n  size: number;\n  step: StepMerged;\n}\n```\n\nMust render a `<span>` since it's placed inside a `<button>` wrapper.\n\n### ArrowRenderProps\n\n```typescript\ntype ArrowRenderProps = {\n  base: number;          // Arrow base width\n  placement: Placement;  // Computed placement\n  size: number;          // Arrow height\n}\n```\n\n### LoaderRenderProps\n\n```typescript\ntype LoaderRenderProps = {\n  step: StepMerged;\n}\n```\n\nSet `loaderComponent={null}` on Props or Step to disable the loader entirely.\n\nDocs: https://v3.react-joyride.com/docs/custom-components\n","CLAUDE.md":"# CLAUDE.md\n\n## Overview\n\nReact Joyride is a React library for creating guided tours (tooltips, walkthroughs, onboarding). Supports React 16.8-19 and SSR.\n\nFor deep architecture details, see `docs/architecture.md`.\n\n## Commands\n\nPackage manager: **pnpm**\n\n```bash\npnpm lint              # ESLint with --fix on src/, test/, and e2e/\npnpm typecheck         # tsc using test/tsconfig.json\npnpm test              # Vitest (run once)\npnpm test:coverage     # With coverage\npnpm test:watch        # Watch mode\npnpm e2e               # Playwright (chromium, firefox, webkit)\npnpm e2e:chromium      # Chromium only\npnpm website           # Website at localhost:3000\npnpm website:build     # Build website for production\npnpm website:start     # Serve built website locally\npnpm website:serve     # Build + start website\npnpm build             # tsdown\npnpm check             # lint → typecheck → test:coverage\npnpm validate          # check → build → size → typevalidation\n```\n\nSingle test: `pnpm test modules/store` | Single e2e: `pnpm e2e:chromium standard`\n\n## How It Works\n\nSingleton `Store` class (ref-based) manages frozen state snapshots. React subscribes via `useSyncExternalStore`.\n\nTwo public APIs: `<Joyride>` component and `useJoyride()` hook (returns `{ controls, failures, on, state, step, Tour }`).\n\n**State machine** has two dimensions: tour `Status` (`idle → ready → waiting → running ↔ paused → finished/skipped`) and step `Lifecycle` (`init → ready → beacon_before → beacon → tooltip_before → tooltip → complete`). See architecture.md for full details.\n\n**Event system**: Single `onEvent(data, controls)` callback receives discriminated events and tour controls: `tour:start`, `step:before_hook`, `step:before`, `scroll:start`, `scroll:end`, `beacon`, `tooltip`, `step:after`, `step:after_hook`, `tour:end`, `tour:status`, `error:target_not_found`, `error`.\n\n**Controlled** (with `stepIndex` prop): Tour pauses at COMPLETE; parent manages index via `onEvent`. **Uncontrolled**: Store manages index internally; supports `initialStepIndex`.\n\n## Key Files\n\n| File | Role |\n|------|------|\n| `src/index.tsx` | SSR-safe wrapper, exports types and literals |\n| `src/hooks/useJoyride.tsx` | Public hook: orchestrates tour, returns controls/state/Tour |\n| `src/hooks/useTourEngine.ts` | Store init, subscriptions, delegates to sub-hooks |\n| `src/hooks/useControls.ts` | Tour control methods (next, prev, start, stop, etc.) |\n| `src/hooks/useEventEmitter.ts` | Centralized event emission with controls |\n| `src/hooks/usePropSync.ts` | Syncs run/steps props to store |\n| `src/hooks/useLifecycleEffect.ts` | 5 effects: action tracking, target resolution, presentation, transitions, tour flow |\n| `src/hooks/useScrollEffect.ts` | Scroll-to-target with placement adjustments |\n| `src/hooks/usePortalElement.ts` | Portal DOM element management |\n| `src/hooks/useTargetPosition.ts` | Target rect tracking via ResizeObserver |\n| `src/hooks/useFocusTrap.ts` | Focus trap for tooltip accessibility |\n| `src/modules/store.ts` | State machine + frozen snapshots |\n| `src/modules/dom.ts` | Element lookup, scroll, visibility |\n| `src/modules/step.ts` | Step merging + validation |\n| `src/modules/helpers.tsx` | Utilities (deepMerge, mergeProps, logDebug) |\n| `src/modules/changes.ts` | State transition detection (treeChanges) |\n| `src/modules/svg.ts` | SVG path generation for overlay/spotlight |\n| `src/defaults.ts` | Default props, locale, options |\n| `src/styles.ts` | Style generation from step |\n| `src/literals/index.ts` | Constants (ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS) |\n\n## Components\n\n| Component | File | Purpose |\n|-----------|------|---------|\n| TourRenderer | `src/components/TourRenderer.tsx` | Conditional rendering, keyboard handler (ESC) |\n| Step | `src/components/Step.tsx` | Validates step + target, renders Floater |\n| Floater | `src/components/Floater.tsx` | Dual Floating UI instances (beacon + tooltip), arrow |\n| Beacon | `src/components/Beacon.tsx` | Animated pulsing indicator |\n| Tooltip | `src/components/Tooltip/index.tsx` | Button handlers, custom component support |\n| DefaultTooltip | `src/components/Tooltip/DefaultTooltip.tsx` | Default tooltip layout |\n| CloseButton | `src/components/Tooltip/CloseButton.tsx` | SVG close icon |\n| Arrow | `src/components/Arrow.tsx` | Tooltip arrow, customizable via `arrowComponent` |\n| Overlay | `src/components/Overlay.tsx` | SVG-based spotlight with path cutout |\n| Loader | `src/components/Loader.tsx` | Loading indicator during target wait/before hooks |\n| Portal | `src/components/Portal.tsx` | React portal wrapper |\n\n## Types\n\nSplit into 8 domain files under `src/types/`: `common`, `components`, `events`, `floating`, `props`, `state`, `step`, `utilities`. Re-exported from `src/types/index.ts`.\n\n## Path Alias\n\n`~/*` maps to `src/*` (tsconfig.json; vitest via native `resolve.tsconfigPaths`; playwright uses website webServer).\n\n## Test Structure\n\n- **Unit tests** (Vitest + jsdom): `test/modules/`, `test/components/`, `test/hooks/`, `test/tours/`\n- **E2E tests** (Playwright): `e2e/` with screenshot snapshots per browser\n- **Fixtures**: `test/__fixtures__/` — Demo components used by both suites\n\nCoverage thresholds: 90% statements, 80% branches, 90% functions, 90% lines.\n\n## Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `@floating-ui/react-dom` | Tooltip/beacon positioning |\n| `@fastify/deepmerge` | Deep object merging (React element aware) |\n| `@gilbarbara/hooks` | useMemoDeepCompare, useMount, usePrevious, useUpdateEffect, useWindowSize |\n| `@gilbarbara/deep-equal` | Deep equality checking |\n| `scroll` / `scrollparent` | Smooth scrolling and scroll parent detection |\n| `react-innertext` | Extract text from React elements |\n| `is-lite` | Type checking utilities |\n\n## Website\n\nNext.js 16 documentation and demo site in `website/`. Imports library via local path (`\"react-joyride\": \"../\"`).\n\n**Stack**: HeroUI v2, Tailwind CSS v4, MDX (rehype-pretty-code + Shiki), next-themes, DocSearch.\n\n**Bundler**: `dev` runs on Turbopack; `build` stays on `--webpack` (`dev:webpack` is the fallback). `next build --turbopack` fails: `@next/mdx`'s Turbopack path never applies the `isServer` vendored-RSC-React alias (its `webpack()` hook does), so server-evaluated modules get the `react-server` React and any interactive HeroUI component's `@react-aria/ssr` `createContext` throws app-wide. The webpack alias in `next.config.ts` masks this; Turbopack has no server-only alias equivalent (a global `resolveAlias` pulls server React into the client bundle). Upstream: nextui-org/nextui#3967, shuding/nextra#4703.\n\n**Documentation** (`website/src/app/docs/`): MDX pages covering getting-started, new-in-v3, how-it-works, hook, props (options, styles, floating-options), step, events, custom-components, exports, accessibility, migration.\n\n**Demos** (`website/src/app/demos/`): 8 interactive examples — overview, controlled, custom-components, scroll, modal, carousel, chat, multi-route.\n\n**Key patterns**:\n- `ConfigContext` — centralized tour configuration for demos\n- `website/src/components/` — 33 components (ConfigPanel, custom tour components, code blocks, navigation)\n- `website/src/config/` — sidebar nav, default values, hero content\n\n## Visual Customization\n- Visual/cosmetic options (colors, borders, sizes) belong in `styles.*`, not as new Options fields.\n- Before adding a new option for appearance, check if it fits an existing or new `styles` key.\n","docs/architecture.md":"# React Joyride Architecture\n\n## Data Flow\n\n```mermaid\nflowchart TD\n    Props --> useJoyride\n    useJoyride --> useTourEngine\n    useTourEngine --> Store\n    Store --> useSyncExternalStore\n    useSyncExternalStore --> TourRenderer\n    TourRenderer --> Step\n    TourRenderer --> Portal\n    Step --> Floater --> Portal\n    Portal --> Overlay & Loader & Beacon & Tooltip\n```\n\nTwo public APIs:\n- `<Joyride>` component — SSR-safe wrapper that calls `useJoyride` internally\n- `useJoyride(props)` hook — returns `{ controls, failures, on, state, step, Tour }`\n\n## State Machine\n\nTwo dimensions drive the tour:\n\n### Tour Status (overall)\n\n```mermaid\nstateDiagram-v2\n    [*] --> idle\n    idle --> ready : steps loaded\n    ready --> waiting : run=true, no steps\n    ready --> running : run=true + steps\n    waiting --> running : size > 0\n    running --> paused : stop()\n    paused --> running : start()\n    running --> finished : last step complete\n    running --> skipped : skip()\n    finished --> ready : reset()\n    skipped --> ready : reset()\n    paused --> ready : reset()\n```\n\n- `idle`: No steps loaded\n- `ready`: Steps loaded, waiting for `run=true`\n- `waiting`: `run=true` but steps loading async (transitions to RUNNING when size > 0)\n- `running`: Tour active\n- `paused`: Stopped mid-tour via `stop()` (can resume with `start()`)\n- `finished`: Tour completed normally\n- `skipped`: Tour ended via `skip()`\n\n### Step Lifecycle (per step)\n\n```mermaid\nstateDiagram-v2\n    [*] --> init\n    init --> ready : target found / before resolved\n    ready --> beacon_before : beacon path\n    ready --> tooltip_before : skip beacon\n    beacon_before --> beacon : scroll done\n    beacon --> tooltip_before : user click\n    tooltip_before --> tooltip : scroll done\n    tooltip --> complete : user action\n    complete --> [*]\n```\n\n- `init`: Step starting, target check pending, `before` hook runs here\n- `ready`: Target found and visible, preparing render\n- `beacon_before`: Beacon positioned, waiting for scroll to complete\n- `beacon`: Showing pulsing beacon\n- `tooltip_before`: Tooltip positioned, waiting for scroll to complete\n- `tooltip`: Showing tooltip content\n- `complete`: Step done, transitioning to next\n\nBeacon is skipped (goes straight to `tooltip_before`) when:\n- `skipBeacon = true`\n- `step.placement = 'center'`\n- `continuous` mode AND action is NEXT or PREV\n\nThe `*_before` phases exist to hold rendering while scroll animation completes. The scroll effect watches for `positioned=true` during these phases, scrolls, then sets `scrolling=false`, which triggers the transition to the final `beacon`/`tooltip` phase.\n\n### Complete State Transition Sequence\n\nA full multi-step tour from mount to finish:\n\n```\nmount: run=true, steps=[...]\n  → start() → status:running, lifecycle:init\n  → tour:start event\n  → [before hook runs if defined → step:before_hook event → waiting:true → loader shown]\n  → lifecycle: init→ready (target check / before hook resolved)\n  → step:before event (action: lastAction ?? update)\n  → lifecycle: ready → *_before (beacon_before or tooltip_before)\n  → scrolling:true, positioned via Floating UI → scroll animation\n  → scroll:start event\n  → scroll:end event → scrolling:false\n  → lifecycle: *_before → beacon or tooltip\n  → beacon event\n  → User clicks beacon → lifecycle: beacon → tooltip_before → tooltip\n  → tooltip event\n  → Focus trap activated\n  → User clicks \"Next\" → controls.next() → lifecycle:complete, index+1\n  → step:after event (reports previous index)\n  → [after hook runs if defined → step:after_hook event]\n  → lifecycle: complete → init (auto-advance in uncontrolled; controlled waits for parent to update stepIndex)\n  → ... repeats for each step ...\n  → Last step: index >= size → status:finished\n  → tour:end event (reports last step)\n  → controls.reset() → status:ready, index:0\n```\n\n## Controlled vs Uncontrolled\n\n**Uncontrolled** (default): Store manages index internally. `next()`/`prev()`/`go()`/`reset()` work. Auto-advances past missing targets. Supports `initialStepIndex` for starting at a specific step.\n\n**Controlled** (with `stepIndex` prop): Tour pauses at each step's COMPLETE lifecycle phase instead of auto-advancing. Parent manages the step index via `onEvent` callbacks. `go()` and `reset()` are disabled in controlled mode.\n\n## Store (`src/modules/store.ts`)\n\nClass managing immutable state via plain objects. Snapshots are `Object.freeze()`d. Created once per component instance via `useRef`. Subscriptions drive React re-renders via `useSyncExternalStore`.\n\n### State Shape\n\n```typescript\n{\n  action: Actions        // What triggered the change\n  controlled: boolean    // true if controlled prop is set\n  index: number          // Current step index\n  lifecycle: Lifecycle   // Step lifecycle phase\n  origin: Origin | null  // What triggered the action (button_close, button_skip, button_primary, keyboard, overlay)\n  positioned: boolean    // Whether floating element has been positioned (internal only)\n  scrolling: boolean     // Whether scroll animation is in progress\n  size: number           // Total steps count\n  status: Status         // Tour status\n  waiting: boolean       // Waiting for before hook or target polling\n}\n```\n\n`positioned` is internal — stripped from public `State` type via `omit(snapshot, 'positioned')`.\n\n### Store Methods\n\n| Method | Description |\n|--------|-------------|\n| `updateState(patch, forceIndex?)` | Merge patch, freeze snapshot, notify listeners. Controlled mode ignores `index` unless `forceIndex=true` |\n| `subscribe(listener)` | Returns unsubscribe function. Used by `useSyncExternalStore` |\n| `getSnapshot()` / `getServerSnapshot()` | Frozen state for `useSyncExternalStore` |\n| `getState()` | Public state (without `positioned`) |\n| `getEventState()` | Same as `getState()` — for event payloads |\n| `setPositionData(name, data)` | Stores beacon/tooltip `PositionData`. Sets `positioned=true` during `*_before` phases |\n| `getPositionData(name)` | Returns cached position data |\n| `cleanupPositionData()` | Clears both position caches (called on step init) |\n| `setSteps(steps)` | Updates internal steps array and size |\n\n`applyTransitions()` auto-promotes `WAITING → RUNNING` when `size > 0`.\n\n## Event System\n\nSingle `onEvent` prop receives `EventData` and `Controls`:\n\n```typescript\ntype EventHandler = (data: EventData, controls: Controls) => void;\n\ntype EventData = TourData & {\n  error: Error | null;      // Only for ERROR events\n  scroll: ScrollData | null; // Only for SCROLL_START/SCROLL_END\n  scrolling: boolean;\n  type: Events;\n  waiting: boolean;\n}\n```\n\nThe `controls` parameter gives event handlers direct access to tour control methods (next, prev, start, stop, etc.).\n\n```mermaid\nsequenceDiagram\n    participant Tour\n    participant Step\n    participant Scroll\n    participant UI\n\n    Tour->>Step: tour:start\n    Step->>Step: step:before_hook\n    Step->>Step: step:before\n    Step->>Scroll: scroll:start\n    Scroll-->>Step: scroll:end\n    Step->>UI: beacon\n    Note over UI: user clicks\n    UI->>UI: tooltip\n    UI->>Step: step:after\n    Step->>Step: step:after_hook\n    Note over Step: repeat per step\n    Step->>Tour: tour:end\n```\n\nAll events: `tour:start`, `step:before_hook`, `step:before`, `scroll:start`, `scroll:end`, `beacon`, `tooltip`, `step:after`, `step:after_hook`, `tour:end`, `tour:status` (on STOP/RESET), `error:target_not_found`, `error`\n\n## Controls (`src/hooks/useControls.ts`)\n\nExtracted hook providing all tour control methods. Created once via `useMemo`, returned to consumers.\n\n| Method | Effect |\n|--------|--------|\n| `close(origin?)` | RUNNING only. Sets `action:CLOSE, index+1, lifecycle:COMPLETE` |\n| `go(nextIndex)` | RUNNING + uncontrolled only. Sets `action:GO, index=nextIndex, lifecycle:COMPLETE`. FINISHED if index >= size |\n| `info()` | Returns current public state |\n| `next()` | RUNNING only. Sets `action:NEXT, index+1 (clamped to size), lifecycle:COMPLETE` |\n| `open()` | RUNNING only. Sets `lifecycle:TOOLTIP_BEFORE` to force-open tooltip |\n| `prev()` | RUNNING only. Sets `action:PREV, index-1 (clamped to 0), lifecycle:COMPLETE` |\n| `replay(origin?)` | RUNNING + lifecycle:TOOLTIP only. Sets `action:REPLAY, lifecycle:COMPLETE`. Index unchanged. Triggers full step re-entry through `step:after` → `step:before` → BEACON/TOOLTIP |\n| `reset(restart?)` | Uncontrolled only. Sets `index:0, lifecycle:INIT, status:READY (or RUNNING if restart)` |\n| `skip(origin?)` | RUNNING only. Sets `action:SKIP, lifecycle:COMPLETE, status:SKIPPED` |\n| `start(nextIndex?)` | Sets `action:START, lifecycle:INIT, status:RUNNING (or WAITING if no steps)`. Uses `forceIndex=true` |\n| `stop(advance?)` | Not if FINISHED/SKIPPED. Sets `action:STOP, lifecycle:COMPLETE, status:PAUSED`. `index+1` if advance |\n\nAll methods also reset `positioned:false, scrolling:false, waiting:false`.\n\n## Orchestration Hooks\n\n### useJoyride (`src/hooks/useJoyride.tsx`)\n\nPublic hook. Calls `useTourEngine`, wraps state (strips `positioned`), creates `TourRenderer` element, returns `{ controls, failures, on, state, step, Tour }`.\n\n### useTourEngine (`src/hooks/useTourEngine.ts`)\n\nCore orchestrator. Creates store once via `useRef(createStore(mergedProps))`, subscribes via `useSyncExternalStore`. Merges props with defaults via `useMemoDeepCompare`.\n\nOn mount: if `run=true` and steps exist and valid, calls `controls.start()`.\n\nDelegates to five sub-hooks:\n- `useControls` — tour control methods\n- `useEventEmitter` — centralized event emission\n- `usePropSync` — external prop change handling\n- `useLifecycleEffect` — state machine progression\n- `useScrollEffect` — scroll-to-target\n\n### useEventEmitter (`src/hooks/useEventEmitter.ts`)\n\nCentralizes all `onEvent` calls into a single `emitEvent(type, step, overrides?)` function. Uses refs for `onEvent` and `controls` to keep callback identity stable. Composes event data from `store.getEventState()` + defaults (`error: null`, `scroll: null`) + step + type + overrides. Passes `controls` as second argument to `onEvent`.\n\nUsed by `usePropSync`, `useLifecycleEffect`, and `useScrollEffect` instead of calling `onEvent` directly.\n\n### usePropSync (`src/hooks/usePropSync.ts`)\n\nHandles external prop changes:\n\n- **Steps change**: Validates steps, calls `store.setSteps(steps)`. Fires `error` event if invalid.\n- **Run toggle**: `run=true` → `controls.start()`, `run=false` → `controls.stop()`\n\n### useLifecycleEffect (`src/hooks/useLifecycleEffect.ts`)\n\nFive separate effects drive the state machine:\n\n**Effect 1: Action tracking** (`deps: [action]`)\nTracks NEXT/PREV/SKIP/CLOSE actions in `lastAction` ref for enriching callbacks.\n\n**Effect 2: Target resolution** (`deps: [index, lifecycle, status, store]`)\nWhen RUNNING + step exists + lifecycle is INIT:\n1. Cleans up position data\n2. If step has `before` hook: sets `waiting=true`, fires `STEP_BEFORE_HOOK`, runs the async hook with AbortController + timeout. On resolve/reject/timeout → sets `lifecycle:READY, waiting:false`\n3. If no `before` hook: checks target visibility\n   - Target visible → `lifecycle:READY`\n   - `targetWaitTimeout=0` → `lifecycle:READY` (proceed without target)\n   - Otherwise → polls at 100ms intervals, sets `waiting=true`. On timeout → `lifecycle:READY`\n\n**Effect 3: Step presentation** (`deps: [lifecycle, store]`)\nWhen lifecycle reaches READY:\n- Target visible + from INIT → fires `STEP_BEFORE` event\n- Target visible → determines final lifecycle (BEACON or TOOLTIP via `hideBeacon()`), checks if scroll needed, sets `lifecycle:*_BEFORE, scrolling:willScroll`\n- Target not visible (RUNNING, not INIT/COMPLETE) → fires `TARGET_NOT_FOUND`, auto-advances in uncontrolled mode\n\n**Effect 4: Transitions + callbacks** (`deps: [lifecycle, positioned, scrolling, store]`)\n- `*_BEFORE` phase + `scrolling=false` → transitions to final BEACON/TOOLTIP\n- BEACON lifecycle → fires `beacon` event\n- TOOLTIP lifecycle → fires `tooltip` event\n- COMPLETE lifecycle (from TOOLTIP) → fires `step:after` event, runs `after` hook (fire-and-forget), fires `step:after_hook` event\n\n**Effect 5: Tour flow** (`deps: [action, index, lifecycle, size, status, store]`)\n- Action REPLAY + COMPLETE → writes `lifecycle:INIT` (works in both controlled and uncontrolled modes; bypasses the controlled-mode pause and the FINISHED branch on the last step to allow same-index re-entry)\n- No step + lifecycle INIT → sets FINISHED\n- Uncontrolled + COMPLETE + index < size → auto-advances to INIT (controlled mode pauses here, waiting for parent to update `stepIndex`)\n- COMPLETE + index >= size → sets FINISHED\n- Status → FINISHED/SKIPPED → fires `tour:end`, calls `controls.reset()`, clears lastAction\n- Status → RUNNING (from IDLE/READY/PAUSED) → fires `tour:start`\n- Action → STOP → fires `tour:status`, clears lastAction\n- Action → RESET → fires `tour:status`, clears lastAction\n\n### useScrollEffect (`src/hooks/useScrollEffect.ts`)\n\nScrolls to target when lifecycle reaches `*_before` phases and `positioned=true` and `scrolling=true`:\n\n- Custom scroll parent: first scrolls page to bring parent into view, then scrolls within parent\n- `adjustForPlacement()`: accounts for placement (top, left, right) to avoid tooltip being off-screen after scroll\n- Fires `scroll:start` and `scroll:end` events with `ScrollData` (initial, target, element, duration)\n- Sets `scrolling:false` on completion, triggering Effect 4's `*_BEFORE → BEACON/TOOLTIP` transition\n- Cancels previous scroll if new one starts\n\n## Step Processing\n\n### Step Merging (`src/modules/step.ts`)\n\n`getMergedStep(props, currentStep)` merges in two passes:\n\n**Pass 1 — step merge:**\n```\ndefaultStep → pick(props, basePropsFields) → currentStep\n```\n\n**Pass 2 — options merge:**\n```\ndefaultOptions → props.options → pick(currentStep, optionFieldNames)\n```\n\nFinal overrides applied after merge:\n- `locale`: deep-merged `defaultLocale → props.locale → step.locale`\n- `floatingOptions`: deep-merged `defaultFloatingOptions → props.floatingOptions → step.floatingOptions`\n- `spotlightPadding`: normalized to `{ top, right, bottom, left }` via `normalizeSpotlightPadding()`\n- `styles`: generated via `getStyles(step)`\n\nSharedProps fields picked from props: `arrowComponent, beaconComponent, floatingOptions, loaderComponent, locale, styles, tooltipComponent`\n\n### Step Hooks\n\nTwo hooks per step, defined as Options (globally or per-step):\n\n**`before(data: TourData) → Promise<void>`**: Blocks the tour at INIT phase. Sets `waiting=true` immediately; shows loader after `loaderDelay` (default 300ms). Capped by `beforeTimeout` (default 5000ms). On error/timeout, fires `error` event and proceeds.\n\n**`after(data: TourData) → void`**: Fire-and-forget after STEP_AFTER. Wrapped in try-catch — user code errors don't break the tour.\n\n### Target Resolution & Tracking\n\n**Resolution** (`getElement()` in `src/modules/dom.ts`):\n- CSS selector string → `document.querySelector()`\n- HTMLElement → used directly\n- React ref → `.current`\n- Function → called, returns element or null\n\n**Visibility** (`isElementVisible()`): walks DOM tree checking `display`/`visibility`.\n\n**Position tracking** (`src/hooks/useTargetPosition.ts`):\n- ResizeObserver on target element\n- Scroll/resize event listeners (passive)\n- Returns `{ top, left, width, height, isFixed }` with spotlightPadding applied\n- Disabled during scroll/waiting to avoid jitter\n\n## Rendering Pipeline\n\n```mermaid\nflowchart TD\n    J[\"Joyride (SSR guard)\"] --> UJ[useJoyride]\n    UJ --> TR[\"TourRenderer (fragment)\"]\n    TR --> S[\"Step\"]\n    TR --> P[\"Portal element\"]\n    S --> FT[useFocusTrap]\n    S --> F[Floater]\n    F --> P\n    P --> B[\"Beacon (BEACON phases)\"]\n    P --> T[\"Tooltip (TOOLTIP phases)\"]\n    P --> A[Arrow]\n    P --> L[\"Loader (waiting=true)\"]\n    P --> O[\"Overlay (SVG spotlight)\"]\n\n    style P fill:#f9f,stroke:#333\n```\n\nAll visible UI renders through Portal to the same portal element — `TourRenderer` and `Step` produce only React fragments, not DOM elements. Floater wraps beacon/tooltip/arrow in `<Portal>`, and TourRenderer wraps overlay/loader in `<Portal>`. Both target the same element from `usePortalElement`.\n\n### Key Components\n\n**Floater** (`src/components/Floater.tsx`):\n- Wraps all rendered content (beacon or tooltip + arrow) in `<Portal>` — nothing renders inline\n- Two `useFloating()` instances: tooltip (full middleware) and beacon (offset only)\n- Tooltip middleware: `offset` (includes spotlightPadding + arrow size), `flip`/`autoPlacement`, `shift`, `arrow`, plus custom middleware from `floatingOptions`\n- Center placement: custom middleware positions at viewport center, uses virtual reference element\n- Strategy: `fixed` when `isFixed=true` or target has fixed/sticky positioning, `absolute` otherwise\n- Reports position data to store via `setPositionData()` on each calculation\n\n**Overlay** (`src/components/Overlay.tsx`):\n- SVG-based with `fillRule=\"evenodd\"` for spotlight cutout\n- `useTargetPosition` tracks target rect via ResizeObserver + scroll/resize listeners\n- Two SVG paths: overlay path (full coverage with cutout) + cover path (fades out to reveal spotlight)\n- Visible during TOOLTIP/TOOLTIP_BEFORE (continuous) or TOOLTIP only (non-continuous); also during waiting\n- Hidden during BEACON/BEACON_BEFORE phases\n\n**Beacon** (`src/components/Beacon.tsx`):\n- Pulsing animation via CSS keyframes (injected once via style tag with existence check)\n- Default: button with inner/outer animated spans\n- Custom: `beaconComponent` prop receives render props\n- Auto-focuses on mount if `shouldFocus=true`\n\n**Tooltip** (`src/components/Tooltip/index.tsx`):\n- Generates button props (back, close, primary, skip) with handlers\n- Primary button: \"Next\" (continuous + not last), \"Last\" (last step), \"Close\" (non-continuous)\n- Delegates to DefaultTooltip (default) or `tooltipComponent` (custom)\n\n**DefaultTooltip** (`src/components/Tooltip/DefaultTooltip.tsx`):\n- Default layout: title → content → footer (skip, back, primary buttons) → close button\n- Footer hidden if `buttons` array has no footer buttons (back/primary/skip)\n\n**Arrow** (`src/components/Arrow.tsx`):\n- Default: SVG arrow element positioned by Floating UI middleware\n- Custom: `arrowComponent` prop receives `ArrowRenderProps`\n\n**Loader** (`src/components/Loader.tsx`):\n- Shown when `waiting=true` (target polling or `before` hook)\n- Custom: `loaderComponent` prop. Set to `null` to disable\n\n**Portal** (`src/components/Portal.tsx`):\n- `createPortal(children, element)` wrapper\n\n**usePortalElement** (`src/hooks/usePortalElement.ts`):\n- Creates `<div id=\"react-joyride-portal\">` in body, or uses provided `portalElement` (selector or HTMLElement)\n- Cleans up auto-created div on unmount\n\n## Appendix\n\n### DOM Utilities (`src/modules/dom.ts`)\n\n- `hasPosition(el, type?)`: Recursively checks for fixed/sticky positioning\n- `getScrollParent(element)`: Uses `scrollparent` library\n- `hasCustomScrollParent(element)`: Checks if scroll parent differs from document\n- `getScrollTo(element, offset)`: Calculates scroll position\n- `getScrollTargetToCenter(element)`: Centers element in viewport\n- `scrollTo(value, options)`: Smooth scroll via `scroll` library; returns `{ cancel, promise }`\n- `scrollDocument()`: Returns document scrolling element\n- `getDocumentHeight(median?)`: Takes max or median of 5 height measurements\n- `canUseDOM()`: SSR safety check\n\n### Focus Trap (`src/hooks/useFocusTrap.ts`)\n\n- Intercepts Tab key within tooltip element\n- Wraps focus forward/backward at tabbable element boundaries\n- Focuses initial selector (`[data-action=primary]`) on mount\n- Disabled when `step.disableFocusTrap=true`\n- Restores previous focus on cleanup\n\n### Styles (`src/styles.ts`)\n\n`getStyles(step)`:\n1. Merges step styles with generated defaults\n2. Adaptive width: `min(step.width, window.innerWidth - 30)`\n3. Generates style objects (beacon, buttons, tooltip, overlay, arrow, etc.)\n4. Returns merged with user overrides\n\nSee `src/defaults.ts` for current default values.\n\n### Types (`src/types/`)\n\n| File | Description |\n|------|-------------|\n| `common.ts` | Actions, Events, Lifecycle, Origin, Status, Placement, Locale, Options, Styles |\n| `components.ts` | ArrowRenderProps, BeaconRenderProps, LoaderRenderProps, TooltipRenderProps |\n| `events.ts` | EventData, EventHandler `(data, controls)`, TourData, ScrollData |\n| `floating.ts` | FloatingOptions, PositionData |\n| `props.ts` | Props, SharedProps, UseJoyrideReturn, StepFailure |\n| `state.ts` | Controls, State |\n| `step.ts` | Step, StepMerged, StepTarget, SelectorOrElement |\n| `utilities.ts` | PartialDeep, Simplify, SetRequired, ValueOf |\n\n### Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `@floating-ui/react-dom` | Tooltip/beacon positioning (dual instances) |\n| `@fastify/deepmerge` | Deep object merging (React element aware) |\n| `@gilbarbara/hooks` | useMemoDeepCompare, useMount, usePrevious, useUpdateEffect, useWindowSize |\n| `@gilbarbara/deep-equal` | Deep equality checking (store change detection) |\n| `@gilbarbara/types` | Utility types |\n| `scroll` / `scrollparent` | Smooth scrolling and scroll parent detection |\n| `react-innertext` | Extract text from React elements (button labels) |\n| `is-lite` | Type checking utilities |\n","skills/react-joyride/references/api-props-options.md":"# Props, Options, Locale, FloatingOptions, Styles\n\n## Props\n\n```typescript\ntype Props = SharedProps & {\n  continuous?: boolean;        // Sequential play with Next button (default: false)\n  debug?: boolean;             // Log to console (default: false)\n  initialStepIndex?: number;   // Start index for uncontrolled mode (default: 0)\n  nonce?: string;              // CSP nonce for inline styles\n  onEvent?: EventHandler;      // (data: EventData, controls: Controls) => void\n  options?: Partial<Options>;  // Default options for all steps\n  portalElement?: string | HTMLElement; // Render tooltip into this element\n  run?: boolean;               // Start/stop tour (default: false)\n  scrollToFirstStep?: boolean; // Scroll to first step on start (default: false)\n  stepIndex?: number;          // Controlled mode: manages step index externally\n  steps: Array<Step>;          // Required: tour steps\n}\n```\n\n## SharedProps\n\nInherited by both `Props` and `Step`:\n\n```typescript\ntype SharedProps = {\n  arrowComponent?: ElementType<ArrowRenderProps>;\n  beaconComponent?: ElementType<BeaconRenderProps>;\n  floatingOptions?: Partial<FloatingOptions>;\n  loaderComponent?: ElementType<LoaderRenderProps> | null; // null disables loader\n  locale?: Locale;\n  styles?: PartialDeep<Styles>;\n  tooltipComponent?: ElementType<TooltipRenderProps>;\n}\n```\n\n## Options\n\nAll fields can be set globally via `options` prop or per-step (step overrides global).\n\n### Appearance\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `backgroundColor` | `string` | `'#ffffff'` | Tooltip background |\n| `primaryColor` | `string` | `'#000000'` | Primary button and beacon |\n| `textColor` | `string` | `'#000000'` | Tooltip text |\n| `overlayColor` | `string` | `'#00000080'` | Overlay backdrop |\n| `arrowColor` | `string` | `'#ffffff'` | Arrow fill |\n| `width` | `string \\| number` | `380` | Tooltip width |\n| `zIndex` | `number` | `100` | z-index for overlay/tooltip |\n\n### Arrow\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `arrowBase` | `number` | `32` | Base width in pixels |\n| `arrowSize` | `number` | `16` | Height/depth in pixels |\n| `arrowSpacing` | `number` | `12` | Distance from tooltip edge |\n\n### Beacon\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `beaconSize` | `number` | `36` | Diameter in pixels |\n| `beaconTrigger` | `'click' \\| 'hover'` | `'click'` | How to open tooltip from beacon |\n\n### Overlay & Spotlight\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `hideOverlay` | `boolean` | `false` | Don't show overlay |\n| `blockTargetInteraction` | `boolean` | `false` | Block pointer events on target |\n| `spotlightRadius` | `number` | `4` | Border radius of cutout |\n| `spotlightPadding` | `number \\| SpotlightPadding` | `10` | Padding around target |\n\n```typescript\ninterface SpotlightPadding { top?: number; right?: number; bottom?: number; left?: number; }\n```\n\n### Buttons & Interactions\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `buttons` | `ButtonType[]` | `['back','close','primary']` | Buttons in tooltip |\n| `closeButtonAction` | `'close' \\| 'skip'` | `'close'` | Close button behavior |\n| `overlayClickAction` | `'close' \\| 'next' \\| false` | `'close'` | Overlay click behavior |\n| `dismissKeyAction` | `'close' \\| 'next' \\| false` | `'close'` | ESC key behavior |\n| `showProgress` | `boolean` | `false` | Show \"N of Total\" in tooltip |\n\n`ButtonType = 'back' | 'close' | 'primary' | 'skip'`\n\n### Scroll\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `skipBeacon` | `boolean` | `false` | Skip beacon, show tooltip directly |\n| `skipScroll` | `boolean` | `false` | Don't scroll to target |\n| `scrollDuration` | `number` | `300` | Scroll animation ms |\n| `scrollOffset` | `number` | `20` | Scroll distance from element |\n| `offset` | `number` | `10` | Distance between tooltip and spotlight |\n\n### Timing & Async\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `before` | `BeforeHook` | - | `(data: TourData) => Promise<void>` — runs before step |\n| `after` | `AfterHook` | - | `(data: TourData) => void` — runs after step (fire-and-forget) |\n| `beforeTimeout` | `number` | `5000` | Max wait for before hook (0 = no timeout) |\n| `targetWaitTimeout` | `number` | `1000` | Max wait for target to appear (0 = no waiting) |\n| `loaderDelay` | `number` | `300` | Delay before showing loader |\n| `disableFocusTrap` | `boolean` | `false` | Disable focus trap on tooltip |\n\n## Locale\n\n```typescript\ninterface Locale {\n  back?: ReactNode;              // 'Back'\n  close?: ReactNode;             // 'Close'\n  last?: ReactNode;              // 'Last'\n  next?: ReactNode;              // 'Next'\n  nextWithProgress?: ReactNode;  // 'Next ({current} of {total})'\n  open?: ReactNode;              // 'Open the dialog'\n  skip?: ReactNode;              // 'Skip'\n}\n```\n\n`{current}` and `{total}` are placeholder strings replaced at render time in `nextWithProgress`.\n\n## FloatingOptions\n\nControls `@floating-ui/react-dom` positioning:\n\n```typescript\ninterface FloatingOptions {\n  autoUpdate?: Partial<AutoUpdateOptions>;     // Auto-reposition options\n  beaconOptions?: { offset?: number };         // Beacon offset (default: -18)\n  flipOptions?: Partial<FlipOptions> | false;  // Flip middleware (false to disable)\n  hideArrow?: boolean;                         // Hide arrow (default: false)\n  middleware?: Array<Middleware>;               // Additional Floating UI middleware\n  onPosition?: (data: PositionData) => void;   // Position callback\n  shiftOptions?: Partial<ShiftOptions>;        // Shift middleware options\n  strategy?: 'fixed' | 'absolute';             // Auto-detected from step.isFixed\n}\n```\n\nDocs: https://v3.react-joyride.com/docs/props/floating-options\n\n## Styles\n\nOverride CSS for any UI element:\n\n```typescript\ninterface Styles {\n  arrow: CSSProperties;\n  beacon: CSSProperties;\n  beaconInner: CSSProperties;\n  beaconOuter: CSSProperties;\n  beaconWrapper: CSSProperties;\n  buttonBack: CSSProperties;\n  buttonClose: CSSProperties;\n  buttonPrimary: CSSProperties;\n  buttonSkip: CSSProperties;\n  floater: CSSProperties;\n  loader: CSSProperties;\n  overlay: CSSProperties;\n  tooltip: CSSProperties;\n  tooltipContainer: CSSProperties;\n  tooltipContent: CSSProperties;\n  tooltipFooter: CSSProperties;\n  tooltipFooterSpacer: CSSProperties;\n  tooltipTitle: CSSProperties;\n}\n```\n\nUse `PartialDeep<Styles>` — only override what you need.\n\nDocs: https://v3.react-joyride.com/docs/props/styles\n","skills/react-joyride/references/patterns.md":"# Common Patterns\n\n## Async Transitions with `before`/`after` Hooks (Preferred)\n\nThe library handles waiting for async operations. Use `before` hooks to prepare the UI before a step shows (open dropdowns, load data, trigger animations). The tour waits for the promise to resolve. Use `after` hooks for cleanup (fire-and-forget, does not block).\n\nThis is the **preferred approach** over controlled mode for most async scenarios.\n\n```tsx\nimport { useState } from 'react';\nimport { ACTIONS, STATUS, useJoyride } from 'react-joyride';\n\nfunction Dashboard() {\n  const [run, setRun] = useState(false);\n  const [isMenuOpen, setIsMenuOpen] = useState(false);\n\n  const { Tour } = useJoyride({\n    continuous: true,\n    run,\n    steps: [\n      {\n        target: '.nav-bar',\n        content: 'This is the navigation',\n        skipBeacon: true,\n      },\n      {\n        target: '.dropdown-item', // Element inside the dropdown\n        content: 'This dropdown item is important',\n        skipBeacon: true,\n        before: ({ action }) => {\n          // Open dropdown before this step — runs on both forward and back navigation\n          setIsMenuOpen(true);\n          return new Promise(resolve => setTimeout(resolve, 300));\n        },\n      },\n      {\n        target: '.main-content',\n        content: 'And here is the main content',\n        before: ({ action }) => {\n          // Close dropdown when moving to this step\n          setIsMenuOpen(false);\n          return new Promise(resolve => setTimeout(resolve, 300));\n        },\n      },\n    ],\n    onEvent: (data) => {\n      if ([STATUS.FINISHED, STATUS.SKIPPED].includes(data.status)) {\n        setRun(false);\n        setIsMenuOpen(false);\n      }\n    },\n  });\n\n  return (\n    <div>\n      {Tour}\n      <button onClick={() => setRun(true)}>Start Tour</button>\n      {/* ... */}\n    </div>\n  );\n}\n```\n\n## Before/After Hooks\n\n### Async data loading before a step\n\n```tsx\n{\n  target: '.user-profile',\n  content: 'Here is your profile data',\n  before: async () => {\n    await fetchUserProfile();\n    // Tour waits for this promise to resolve\n  },\n  beforeTimeout: 10000, // Allow up to 10s\n}\n```\n\n### Analytics tracking after a step\n\n```tsx\n{\n  target: '.feature',\n  content: 'Check out this feature',\n  after: (data) => {\n    // Fire-and-forget — does not block the tour\n    analytics.track('tour_step_completed', {\n      stepIndex: data.index,\n      action: data.action,\n    });\n  },\n}\n```\n\n### Delayed transitions (e.g., waiting for animation)\n\n```tsx\n{\n  target: '.sidebar',\n  content: 'The sidebar',\n  before: ({ action }) => {\n    // Only delay when navigating forward (not on back)\n    const ms = action === ACTIONS.PREV ? 0 : 300;\n    return new Promise(resolve => setTimeout(resolve, ms));\n  },\n}\n```\n\n## Custom Tooltip Component\n\n```tsx\nimport type { TooltipRenderProps } from 'react-joyride';\n\nfunction CustomTooltip({\n  backProps,\n  closeProps,\n  index,\n  isLastStep,\n  primaryProps,\n  size,\n  skipProps,\n  step,\n  tooltipProps,\n}: TooltipRenderProps) {\n  return (\n    <div\n      {...tooltipProps}\n      style={{\n        background: '#fff',\n        borderRadius: 8,\n        maxWidth: 400,\n        padding: 20,\n        width: step.width,\n      }}\n    >\n      {step.title && <h3 style={{ margin: '0 0 8px' }}>{step.title}</h3>}\n      <div>{step.content}</div>\n\n      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 16 }}>\n        {step.buttons.includes('skip') && !isLastStep && (\n          <button {...skipProps} type=\"button\">Skip</button>\n        )}\n        <div style={{ display: 'flex', gap: 8, marginLeft: 'auto' }}>\n          {index > 0 && (\n            <button {...backProps} type=\"button\">Back</button>\n          )}\n          <button {...primaryProps} type=\"button\">\n            {isLastStep ? 'Done' : `Next (${index + 1}/${size})`}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n// Usage:\n// <Joyride tooltipComponent={CustomTooltip} ... />\n// or in useJoyride: useJoyride({ tooltipComponent: CustomTooltip, ... })\n```\n\n## Event Subscription with `on()`\n\n```tsx\nfunction App() {\n  const { on, Tour } = useJoyride({\n    steps,\n    run: true,\n    continuous: true,\n  });\n\n  useEffect(() => {\n    const unsubs = [\n      on('tour:start', (data) => {\n        console.log('Tour started');\n      }),\n      on('tooltip', (data) => {\n        analytics.track('step_viewed', { index: data.index });\n      }),\n      on('tour:end', (data) => {\n        const wasSkipped = data.status === 'skipped';\n        analytics.track('tour_ended', { skipped: wasSkipped, lastStep: data.index });\n      }),\n    ];\n\n    return () => unsubs.forEach(unsub => unsub());\n  }, [on]);\n\n  return <div>{Tour}</div>;\n}\n```\n\n## Dynamic Steps\n\n```tsx\nfunction App() {\n  const [steps, setSteps] = useState<Step[]>([]);\n\n  useEffect(() => {\n    // Build steps based on feature flags, user role, etc.\n    const dynamicSteps: Step[] = [\n      { target: '.dashboard', content: 'Welcome to your dashboard' },\n    ];\n\n    if (user.isAdmin) {\n      dynamicSteps.push({\n        target: '.admin-panel',\n        content: 'Admin controls are here',\n      });\n    }\n\n    if (featureFlags.newSearch) {\n      dynamicSteps.push({\n        target: '.search-bar',\n        content: 'Try the new search',\n      });\n    }\n\n    setSteps(dynamicSteps);\n  }, [user, featureFlags]);\n\n  return (\n    <Joyride\n      run={steps.length > 0}\n      steps={steps}\n      continuous\n    />\n  );\n}\n```\n\n## React Ref Targets\n\n```tsx\nfunction App() {\n  const sidebarRef = useRef<HTMLDivElement>(null);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n\n  const steps: Step[] = [\n    { target: sidebarRef, content: 'Navigation sidebar' },\n    { target: buttonRef, content: 'Click here to create' },\n    { target: '.css-selector', content: 'Mix refs with selectors' },\n  ];\n\n  return (\n    <div>\n      <Joyride steps={steps} run continuous />\n      <div ref={sidebarRef}>Sidebar</div>\n      <button ref={buttonRef}>Create</button>\n      <span className=\"css-selector\">Other element</span>\n    </div>\n  );\n}\n```\n\n## Restart / Resume Tour\n\n```tsx\nfunction App() {\n  const [run, setRun] = useState(false);\n  const [initialStepIndex, setInitialStepIndex] = useState(0);\n\n  const handleStart = () => {\n    setInitialStepIndex(0);\n    setRun(true);\n  };\n\n  const handleResume = (fromStep: number) => {\n    setInitialStepIndex(fromStep);\n    setRun(true);\n  };\n\n  return (\n    <div>\n      <Joyride\n        run={run}\n        initialStepIndex={initialStepIndex}\n        steps={steps}\n        continuous\n        onEvent={(data) => {\n          if ([STATUS.FINISHED, STATUS.SKIPPED].includes(data.status)) {\n            setRun(false);\n          }\n        }}\n      />\n      <button onClick={handleStart}>Start Tour</button>\n      <button onClick={() => handleResume(3)}>Resume from Step 4</button>\n    </div>\n  );\n}\n```\n\n## Center Placement (Modal Style)\n\n```tsx\n{\n  target: 'body',\n  placement: 'center',\n  content: (\n    <div>\n      <h2>Welcome!</h2>\n      <p>This appears as a centered modal overlay.</p>\n    </div>\n  ),\n  // Center placement automatically hides beacon and arrow\n}\n```\n","skills/react-joyride/SKILL.md":"---\nname: react-joyride\ndescription: >-\n  Guide for implementing, configuring, and debugging React Joyride v3 guided tours.\n  Use this skill whenever the user mentions joyride, guided tour, onboarding tour,\n  walkthrough, tooltip tour, step-by-step guide, product tour, or wants to highlight\n  UI elements sequentially. Also use when debugging tour issues like tooltips not\n  appearing, targets not found, or controlled mode problems. This skill covers the\n  useJoyride hook, Joyride component, step configuration, events, controls, custom\n  components, and styling.\nlicense: MIT\nmetadata:\n  author: gilbarbara\n  version: \"3.0.0\"\n---\n\n# React Joyride v3\n\nCreate guided tours in React apps. Two public APIs: the `useJoyride()` hook (recommended) and the `<Joyride>` component.\n\nOnline docs: https://v3.react-joyride.com\n\n## Quick Start\n\n### Using the hook (recommended)\n\n```tsx\nimport { useJoyride, STATUS, Status } from 'react-joyride';\n\nfunction App() {\n  const { Tour } = useJoyride({\n    continuous: true,\n    run: true,\n    steps: [\n      { target: '.my-element', content: 'This is the first step', title: 'Welcome' },\n      { target: '#sidebar', content: 'Navigate here', placement: 'right' },\n    ],\n    onEvent: (data) => {\n      if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n        // Tour ended\n      }\n    },\n  });\n\n  return <div>{Tour}{/* rest of app */}</div>;\n}\n```\n\n### Using the component\n\n```tsx\nimport { Joyride, STATUS, Status } from 'react-joyride';\n\nfunction App() {\n  return (\n    <Joyride\n      continuous\n      run={true}\n      steps={[\n        { target: '.my-element', content: 'First step' },\n        { target: '#sidebar', content: 'Second step' },\n      ]}\n      onEvent={(data) => {\n        if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n          // Tour ended\n        }\n      }}\n    />\n  );\n}\n```\n\nThe hook returns `{ controls, failures, on, state, step, Tour }`. Render `Tour` in your JSX.\n\nDocs: https://v3.react-joyride.com/docs/getting-started\n\n## Core Concepts\n\nThe tour has two state dimensions:\n\n**Tour Status**: `idle -> ready -> waiting -> running <-> paused -> finished | skipped`\n- `idle`: No steps loaded\n- `ready`: Steps loaded, waiting for `run: true`\n- `waiting`: `run=true` but steps loading async (transitions to running when steps arrive)\n- `running`: Tour active\n- `paused`: Tour paused (controlled mode at COMPLETE, or `stop()` called)\n- `finished` / `skipped`: Tour ended\n\n**Step Lifecycle** (per step): `init -> ready -> beacon_before -> beacon -> tooltip_before -> tooltip -> complete`\n- `*_before` phases: scrolling and positioning happen here\n- `beacon`: Pulsing indicator shown (skipped when `continuous` + navigating, `skipBeacon`, or `placement: 'center'`)\n- `tooltip`: The tooltip is visible and interactive\n\nDocs: https://v3.react-joyride.com/docs/how-it-works\n\n## Step Configuration\n\nEach step requires `target` and `content`. All other fields are optional.\n\n```tsx\n{\n  target: '.my-element',       // CSS selector, HTMLElement, React ref, or () => HTMLElement\n  content: 'Step body text',   // ReactNode\n  title: 'Optional title',    // ReactNode\n  placement: 'bottom',        // Default. Also: top, left, right, *-start, *-end, auto, center\n  id: 'unique-id',            // Optional identifier\n  data: { custom: 'data' },   // Attached to event callbacks\n}\n```\n\n### Target types\n\n```tsx\n// CSS selector\n{ target: '.sidebar-nav' }\n// HTMLElement\n{ target: document.getElementById('my-el') }\n// React ref\nconst ref = useRef(null);\n{ target: ref }\n// Function (evaluated each lifecycle)\n{ target: () => document.querySelector('.dynamic-element') }\n```\n\n### Common step options (override per-step)\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `placement` | `'bottom'` | Tooltip position. Use `'center'` for modal-style (requires `target: 'body'`) |\n| `skipBeacon` | `false` | Skip beacon, show tooltip directly |\n| `buttons` | `['back','close','primary']` | Buttons in tooltip. Add `'skip'` for skip button |\n| `hideOverlay` | `false` | Don't show dark overlay |\n| `blockTargetInteraction` | `false` | Block clicks on highlighted element |\n| `before` | - | `(data) => Promise<void>` — async hook before step shows |\n| `after` | - | `(data) => void` — fire-and-forget hook after step completes |\n| `skipScroll` | `false` | Don't scroll to target |\n| `scrollTarget` | - | Scroll to this element instead of `target` |\n| `spotlightTarget` | - | Highlight this element instead of `target` |\n| `spotlightPadding` | `10` | Padding around spotlight. Number or `{ top, right, bottom, left }` |\n| `targetWaitTimeout` | `1000` | ms to wait for target to appear. `0` = no waiting |\n| `beforeTimeout` | `5000` | ms to wait for `before` hook. `0` = no timeout |\n\nAll `Options` fields can be set globally via `options` prop or per-step. Per-step values override global.\n\nDocs: https://v3.react-joyride.com/docs/step | https://v3.react-joyride.com/docs/props/options\n\n## Uncontrolled vs Controlled\n\n### Uncontrolled (default — strongly preferred)\n\nThe tour manages step navigation internally. This is the right choice for most use cases.\n\n**The library handles async transitions for you.** If a step needs to wait for a UI change (dropdown opening, data loading, animation), use `before` hooks — the tour waits for the promise to resolve before showing the step. If a target element isn't in the DOM yet, `targetWaitTimeout` (default: 1000ms) handles polling for it. You do NOT need controlled mode for these cases.\n\n```tsx\nconst { Tour } = useJoyride({\n  continuous: true,\n  run: isRunning,\n  steps: [\n    { target: '.nav', content: 'Navigation' },\n    {\n      target: '.dropdown-item',\n      content: 'Inside the dropdown',\n      before: () => {\n        // Open dropdown and wait for animation — tour waits automatically\n        openDropdown();\n        return new Promise(resolve => setTimeout(resolve, 300));\n      },\n      after: () => closeDropdown(), // Clean up after step (fire-and-forget)\n    },\n    { target: '.main-content', content: 'Main content' },\n  ],\n  onEvent: (data) => {\n    if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n      setIsRunning(false);\n    }\n  },\n});\n```\n\n### Controlled (with `stepIndex`) — use sparingly\n\nOnly use controlled mode when the parent genuinely needs to manage the step index externally (e.g., syncing with URL params, external state machines, or complex multi-component coordination that `before`/`after` hooks can't handle).\n\n```tsx\nconst [stepIndex, setStepIndex] = useState(0);\nconst [run, setRun] = useState(true);\n\nconst { Tour } = useJoyride({\n  continuous: true,\n  run,\n  stepIndex,  // This makes it controlled\n  steps,\n  onEvent: (data) => {\n    const { action, index, status, type } = data;\n\n    if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(status)) {\n      setRun(false);\n      return;\n    }\n\n    if (type === 'step:after' || type === 'error:target_not_found') {\n      setStepIndex(index + (action === 'prev' ? -1 : 1));\n    }\n  },\n});\n```\n\n**Controlled mode rules:**\n- `go()` and `reset()` are disabled (logged warning)\n- You must update `stepIndex` in response to events\n- The tour pauses at COMPLETE — you must advance it\n- Prefer uncontrolled mode with `before`/`after` hooks unless you have a strong reason for external index management\n\n## Event System\n\n### `onEvent` callback\n\n```tsx\nonEvent: (data: EventData, controls: Controls) => void\n```\n\nThe `data` object contains the full tour state plus event-specific fields. The `controls` object lets you programmatically control the tour.\n\n### Event types (in order per step)\n\n| Event | When |\n|-------|------|\n| `tour:start` | Tour begins |\n| `step:before_hook` | `before` hook is called |\n| `step:before` | Target found, step about to render |\n| `scroll:start` | Scrolling to target |\n| `scroll:end` | Scroll complete |\n| `beacon` | Beacon shown |\n| `tooltip` | Tooltip shown |\n| `step:after` | User navigated (next/prev/close/skip) |\n| `step:after_hook` | `after` hook called |\n| `tour:end` | Tour finished or skipped |\n| `tour:status` | Status changed (on stop/reset) |\n| `error:target_not_found` | Target element not found |\n| `error` | Generic error |\n\n### Event subscription with `on()`\n\n```tsx\nconst { on, Tour } = useJoyride({ ... });\n\nuseEffect(() => {\n  const unsubscribe = on('tooltip', (data, controls) => {\n    analytics.track('tour_step_viewed', { step: data.index });\n  });\n  return unsubscribe;\n}, [on]);\n```\n\nDocs: https://v3.react-joyride.com/docs/events\n\n## Controls\n\nAvailable via `useJoyride()` return value or `onEvent` second argument:\n\n| Method | Description |\n|--------|-------------|\n| `next()` | Advance to next step |\n| `prev()` | Go to previous step |\n| `close(origin?)` | Close current step, advance |\n| `skip(origin?)` | Skip the tour entirely |\n| `start(index?)` | Start the tour |\n| `stop(advance?)` | Stop (pause) the tour |\n| `go(index)` | Jump to step (uncontrolled only) |\n| `reset(restart?)` | Reset tour (uncontrolled only) |\n| `open()` | Open tooltip for current step |\n| `info()` | Get current state |\n\nDocs: https://v3.react-joyride.com/docs/hook\n\n## Styling & Theming\n\nThree layers of customization (from simple to full control):\n\n### 1. Color options (simplest)\n\n```tsx\noptions: {\n  primaryColor: '#1976d2',      // Buttons and beacon\n  backgroundColor: '#1a1a2e',   // Tooltip background\n  textColor: '#ffffff',         // Tooltip text\n  overlayColor: 'rgba(0,0,0,0.7)', // Overlay backdrop\n  arrowColor: '#1a1a2e',        // Arrow (match background)\n}\n```\n\n### 2. Styles override\n\n```tsx\nstyles: {\n  tooltip: { borderRadius: 12 },\n  buttonPrimary: { backgroundColor: '#1976d2' },\n  buttonBack: { color: '#666' },\n  spotlight: { borderRadius: 8 },\n}\n```\n\nStyle keys: `arrow`, `beacon`, `beaconInner`, `beaconOuter`, `beaconWrapper`, `buttonBack`, `buttonClose`, `buttonPrimary`, `buttonSkip`, `floater`, `loader`, `overlay`, `tooltip`, `tooltipContainer`, `tooltipContent`, `tooltipFooter`, `tooltipFooterSpacer`, `tooltipTitle`\n\n### 3. Custom components (full control)\n\nSee next section.\n\nDocs: https://v3.react-joyride.com/docs/props/styles\n\n## Custom Components\n\nReplace any UI component via props. Each receives render props with step data and button handlers.\n\n### Custom Tooltip\n\n```tsx\nimport type { TooltipRenderProps } from 'react-joyride';\n\nfunction MyTooltip({ backProps, index, primaryProps, size, skipProps, step, tooltipProps }: TooltipRenderProps) {\n  return (\n    <div {...tooltipProps} style={{ background: '#fff', padding: 16, borderRadius: 8, width: step.width }}>\n      {step.title && <h3>{step.title}</h3>}\n      <div>{step.content}</div>\n      <div>\n        {index > 0 && <button {...backProps}>Back</button>}\n        <button {...primaryProps}>Next</button>\n      </div>\n    </div>\n  );\n}\n\n// Usage\n<Joyride tooltipComponent={MyTooltip} ... />\n```\n\n**Important**: Spread `tooltipProps` on the container (sets `role=\"dialog\"` and `aria-modal`). Spread button props (`backProps`, `primaryProps`, `closeProps`, `skipProps`) on buttons for correct action handling.\n\n### Custom Beacon\n\nMust render a `<span>` (placed inside a `<button>` wrapper). Receives `BeaconRenderProps`: `{ continuous, index, isLastStep, size, step }`.\n\n### Custom Arrow\n\nReceives `ArrowRenderProps`: `{ base, placement, size }`.\n\n### Custom Loader\n\nReceives `LoaderRenderProps`: `{ step }`. Set to `null` to disable the loader.\n\nDocs: https://v3.react-joyride.com/docs/custom-components\n\n## Problem → Solution Guide\n\n| I need to... | Use |\n|---|---|\n| Wait for async UI changes between steps (dropdown, animation, data load) | `before` hook returning a Promise — not controlled mode |\n| Control step navigation externally (URL sync, external state machine) | Controlled mode with `stepIndex` — but try `before`/`after` hooks first |\n| Track which steps failed (target missing, hook error) | `failures` array from `useJoyride()` return |\n| Listen to specific events without `onEvent` switch | `on('event:type', handler)` from `useJoyride()` return |\n| Show a centered modal-style step | `target: 'body'` + `placement: 'center'` |\n\n## Common Gotchas & Debugging\n\n### Use `debug: true` first\n\nThe `debug` prop is the most powerful troubleshooting tool. It logs detailed lifecycle transitions, state changes, and event emissions to the console. Always start here when something isn't working.\n\n```tsx\n<Joyride debug={true} ... />\n// or\nuseJoyride({ debug: true, ... })\n```\n\nThe console output shows exactly which lifecycle phase the tour reaches, what actions are firing, and where it gets stuck.\n\n### Tour not starting\n- Check `run={true}` is set\n- Verify `steps` array is not empty and steps have valid `target` + `content`\n- For SSR: use `<Joyride>` component (auto-guards DOM access) or check `typeof window !== 'undefined'`\n\n### Target not found\n- Test selector in console: `document.querySelector('.your-selector')`\n- Element must be visible (not `display: none`, `visibility: hidden`, or zero dimensions)\n- If element mounts later, increase `targetWaitTimeout` (default: 1000ms)\n- Set `targetWaitTimeout: 0` to skip waiting entirely\n- In uncontrolled mode, missing targets auto-advance; in controlled mode, handle `error:target_not_found` event\n\n### Tooltip never appears / overlay flashes\n- Add `debug: true` and check the console to see which lifecycle phase is reached\n- Verify the target element is in the viewport or scrollable\n- Check for CSS `overflow: hidden` on ancestors clipping the target\n- If using portals or modals, the target may not be accessible\n\n### Controlled mode stuck\n- First question: do you actually need controlled mode? Most async needs are solved with `before`/`after` hooks in uncontrolled mode\n- If controlled: you MUST update `stepIndex` in your `onEvent` handler when `type === 'step:after'`\n- Handle both forward (`action !== 'prev'`) and backward (`action === 'prev'`) navigation\n- Also handle `error:target_not_found` to skip broken steps\n- `go()` and `reset()` don't work in controlled mode\n\n### Before hook timeout\n- Default `beforeTimeout` is 5000ms — increase if your async operation takes longer\n- Set `beforeTimeout: 0` for no timeout\n- The loader appears after `loaderDelay` (300ms) while waiting\n\n### Scroll issues\n- Use `scrollTarget` to scroll to a different element than the tooltip target\n- Adjust `scrollOffset` (default: 20px) for headers or fixed elements\n- Set `skipScroll: true` to disable auto-scrolling for a step\n- `scrollToFirstStep: false` by default — set to `true` if first step is off-screen\n\n### Center placement\n- Use `placement: 'center'` with `target: 'body'` for modal-style centered tooltips\n- Center placement automatically hides the beacon and arrow\n\n### Imports\n\n```tsx\n// Named exports only (no default export in v3)\nimport { Joyride, useJoyride } from 'react-joyride';\n\n// Constants for type-safe comparisons\nimport { ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS } from 'react-joyride';\n\n// Types\nimport type { Step, Props, EventData, Controls, TooltipRenderProps } from 'react-joyride';\n```\n\nDocs: https://v3.react-joyride.com/docs/exports\n\n## Reference Files\n\nRead these for complete API details:\n\n- `references/api-props-options.md` — Full Props, Options (all 30+ fields with defaults), Locale, FloatingOptions, Styles types\n- `references/api-step-state-controls.md` — Step, StepMerged, StepTarget, State, Controls (all 10 methods), UseJoyrideReturn, StepFailure\n- `references/api-events-components.md` — All 13 event types, ACTIONS/LIFECYCLE/STATUS/ORIGIN constants, EventData, custom component render props\n- `references/patterns.md` — Complete working examples: controlled mode, before/after hooks, custom tooltip, event subscription, dynamic steps\n"},"items":[{"name":"architecture.md","path":"docs/architecture.md","rawUrl":"https://raw.githubusercontent.com/gilbarbara/react-joyride/HEAD/docs/architecture.md","title":"docs - System Architecture & Module Boundaries","category":"project-spec","format":"markdown","content":"# React Joyride Architecture\n\n## Data Flow\n\n```mermaid\nflowchart TD\n    Props --> useJoyride\n    useJoyride --> useTourEngine\n    useTourEngine --> Store\n    Store --> useSyncExternalStore\n    useSyncExternalStore --> TourRenderer\n    TourRenderer --> Step\n    TourRenderer --> Portal\n    Step --> Floater --> Portal\n    Portal --> Overlay & Loader & Beacon & Tooltip\n```\n\nTwo public APIs:\n- `<Joyride>` component — SSR-safe wrapper that calls `useJoyride` internally\n- `useJoyride(props)` hook — returns `{ controls, failures, on, state, step, Tour }`\n\n## State Machine\n\nTwo dimensions drive the tour:\n\n### Tour Status (overall)\n\n```mermaid\nstateDiagram-v2\n    [*] --> idle\n    idle --> ready : steps loaded\n    ready --> waiting : run=true, no steps\n    ready --> running : run=true + steps\n    waiting --> running : size > 0\n    running --> paused : stop()\n    paused --> running : start()\n    running --> finished : last step complete\n    running --> skipped : skip()\n    finished --> ready : reset()\n    skipped --> ready : reset()\n    paused --> ready : reset()\n```\n\n- `idle`: No steps loaded\n- `ready`: Steps loaded, waiting for `run=true`\n- `waiting`: `run=true` but steps loading async (transitions to RUNNING when size > 0)\n- `running`: Tour active\n- `paused`: Stopped mid-tour via `stop()` (can resume with `start()`)\n- `finished`: Tour completed normally\n- `skipped`: Tour ended via `skip()`\n\n### Step Lifecycle (per step)\n\n```mermaid\nstateDiagram-v2\n    [*] --> init\n    init --> ready : target found / before resolved\n    ready --> beacon_before : beacon path\n    ready --> tooltip_before : skip beacon\n    beacon_before --> beacon : scroll done\n    beacon --> tooltip_before : user click\n    tooltip_before --> tooltip : scroll done\n    tooltip --> complete : user action\n    complete --> [*]\n```\n\n- `init`: Step starting, target check pending, `before` hook runs here\n- `ready`: Target found and visible, preparing render\n- `beacon_before`: Beacon positioned, waiting for scroll to complete\n- `beacon`: Showing pulsing beacon\n- `tooltip_before`: Tooltip positioned, waiting for scroll to complete\n- `tooltip`: Showing tooltip content\n- `complete`: Step done, transitioning to next\n\nBeacon is skipped (goes straight to `tooltip_before`) when:\n- `skipBeacon = true`\n- `step.placement = 'center'`\n- `continuous` mode AND action is NEXT or PREV\n\nThe `*_before` phases exist to hold rendering while scroll animation completes. The scroll effect watches for `positioned=true` during these phases, scrolls, then sets `scrolling=false`, which triggers the transition to the final `beacon`/`tooltip` phase.\n\n### Complete State Transition Sequence\n\nA full multi-step tour from mount to finish:\n\n```\nmount: run=true, steps=[...]\n  → start() → status:running, lifecycle:init\n  → tour:start event\n  → [before hook runs if defined → step:before_hook event → waiting:true → loader shown]\n  → lifecycle: init→ready (target check / before hook resolved)\n  → step:before event (action: lastAction ?? update)\n  → lifecycle: ready → *_before (beacon_before or tooltip_before)\n  → scrolling:true, positioned via Floating UI → scroll animation\n  → scroll:start event\n  → scroll:end event → scrolling:false\n  → lifecycle: *_before → beacon or tooltip\n  → beacon event\n  → User clicks beacon → lifecycle: beacon → tooltip_before → tooltip\n  → tooltip event\n  → Focus trap activated\n  → User clicks \"Next\" → controls.next() → lifecycle:complete, index+1\n  → step:after event (reports previous index)\n  → [after hook runs if defined → step:after_hook event]\n  → lifecycle: complete → init (auto-advance in uncontrolled; controlled waits for parent to update stepIndex)\n  → ... repeats for each step ...\n  → Last step: index >= size → status:finished\n  → tour:end event (reports last step)\n  → controls.reset() → status:ready, index:0\n```\n\n## Controlled vs Uncontrolled\n\n**Uncontrolled** (default): Store manages index internally. `next()`/`prev()`/`go()`/`reset()` work. Auto-advances past missing targets. Supports `initialStepIndex` for starting at a specific step.\n\n**Controlled** (with `stepIndex` prop): Tour pauses at each step's COMPLETE lifecycle phase instead of auto-advancing. Parent manages the step index via `onEvent` callbacks. `go()` and `reset()` are disabled in controlled mode.\n\n## Store (`src/modules/store.ts`)\n\nClass managing immutable state via plain objects. Snapshots are `Object.freeze()`d. Created once per component instance via `useRef`. Subscriptions drive React re-renders via `useSyncExternalStore`.\n\n### State Shape\n\n```typescript\n{\n  action: Actions        // What triggered the change\n  controlled: boolean    // true if controlled prop is set\n  index: number          // Current step index\n  lifecycle: Lifecycle   // Step lifecycle phase\n  origin: Origin | null  // What triggered the action (button_close, button_skip, button_primary, keyboard, overlay)\n  positioned: boolean    // Whether floating element has been positioned (internal only)\n  scrolling: boolean     // Whether scroll animation is in progress\n  size: number           // Total steps count\n  status: Status         // Tour status\n  waiting: boolean       // Waiting for before hook or target polling\n}\n```\n\n`positioned` is internal — stripped from public `State` type via `omit(snapshot, 'positioned')`.\n\n### Store Methods\n\n| Method | Description |\n|--------|-------------|\n| `updateState(patch, forceIndex?)` | Merge patch, freeze snapshot, notify listeners. Controlled mode ignores `index` unless `forceIndex=true` |\n| `subscribe(listener)` | Returns unsubscribe function. Used by `useSyncExternalStore` |\n| `getSnapshot()` / `getServerSnapshot()` | Frozen state for `useSyncExternalStore` |\n| `getState()` | Public state (without `positioned`) |\n| `getEventState()` | Same as `getState()` — for event payloads |\n| `setPositionData(name, data)` | Stores beacon/tooltip `PositionData`. Sets `positioned=true` during `*_before` phases |\n| `getPositionData(name)` | Returns cached position data |\n| `cleanupPositionData()` | Clears both position caches (called on step init) |\n| `setSteps(steps)` | Updates internal steps array and size |\n\n`applyTransitions()` auto-promotes `WAITING → RUNNING` when `size > 0`.\n\n## Event System\n\nSingle `onEvent` prop receives `EventData` and `Controls`:\n\n```typescript\ntype EventHandler = (data: EventData, controls: Controls) => void;\n\ntype EventData = TourData & {\n  error: Error | null;      // Only for ERROR events\n  scroll: ScrollData | null; // Only for SCROLL_START/SCROLL_END\n  scrolling: boolean;\n  type: Events;\n  waiting: boolean;\n}\n```\n\nThe `controls` parameter gives event handlers direct access to tour control methods (next, prev, start, stop, etc.).\n\n```mermaid\nsequenceDiagram\n    participant Tour\n    participant Step\n    participant Scroll\n    participant UI\n\n    Tour->>Step: tour:start\n    Step->>Step: step:before_hook\n    Step->>Step: step:before\n    Step->>Scroll: scroll:start\n    Scroll-->>Step: scroll:end\n    Step->>UI: beacon\n    Note over UI: user clicks\n    UI->>UI: tooltip\n    UI->>Step: step:after\n    Step->>Step: step:after_hook\n    Note over Step: repeat per step\n    Step->>Tour: tour:end\n```\n\nAll events: `tour:start`, `step:before_hook`, `step:before`, `scroll:start`, `scroll:end`, `beacon`, `tooltip`, `step:after`, `step:after_hook`, `tour:end`, `tour:status` (on STOP/RESET), `error:target_not_found`, `error`\n\n## Controls (`src/hooks/useControls.ts`)\n\nExtracted hook providing all tour control methods. Created once via `useMemo`, returned to consumers.\n\n| Method | Effect |\n|--------|--------|\n| `close(origin?)` | RUNNING only. Sets `action:CLOSE, index+1, lifecycle:COMPLETE` |\n| `go(nextIndex)` | RUNNING + uncontrolled only. Sets `action:GO, index=nextIndex, lifecycle:COMPLETE`. FINISHED if index >= size |\n| `info()` | Returns current public state |\n| `next()` | RUNNING only. Sets `action:NEXT, index+1 (clamped to size), lifecycle:COMPLETE` |\n| `open()` | RUNNING only. Sets `lifecycle:TOOLTIP_BEFORE` to force-open tooltip |\n| `prev()` | RUNNING only. Sets `action:PREV, index-1 (clamped to 0), lifecycle:COMPLETE` |\n| `replay(origin?)` | RUNNING + lifecycle:TOOLTIP only. Sets `action:REPLAY, lifecycle:COMPLETE`. Index unchanged. Triggers full step re-entry through `step:after` → `step:before` → BEACON/TOOLTIP |\n| `reset(restart?)` | Uncontrolled only. Sets `index:0, lifecycle:INIT, status:READY (or RUNNING if restart)` |\n| `skip(origin?)` | RUNNING only. Sets `action:SKIP, lifecycle:COMPLETE, status:SKIPPED` |\n| `start(nextIndex?)` | Sets `action:START, lifecycle:INIT, status:RUNNING (or WAITING if no steps)`. Uses `forceIndex=true` |\n| `stop(advance?)` | Not if FINISHED/SKIPPED. Sets `action:STOP, lifecycle:COMPLETE, status:PAUSED`. `index+1` if advance |\n\nAll methods also reset `positioned:false, scrolling:false, waiting:false`.\n\n## Orchestration Hooks\n\n### useJoyride (`src/hooks/useJoyride.tsx`)\n\nPublic hook. Calls `useTourEngine`, wraps state (strips `positioned`), creates `TourRenderer` element, returns `{ controls, failures, on, state, step, Tour }`.\n\n### useTourEngine (`src/hooks/useTourEngine.ts`)\n\nCore orchestrator. Creates store once via `useRef(createStore(mergedProps))`, subscribes via `useSyncExternalStore`. Merges props with defaults via `useMemoDeepCompare`.\n\nOn mount: if `run=true` and steps exist and valid, calls `controls.start()`.\n\nDelegates to five sub-hooks:\n- `useControls` — tour control methods\n- `useEventEmitter` — centralized event emission\n- `usePropSync` — external prop change handling\n- `useLifecycleEffect` — state machine progression\n- `useScrollEffect` — scroll-to-target\n\n### useEventEmitter (`src/hooks/useEventEmitter.ts`)\n\nCentralizes all `onEvent` calls into a single `emitEvent(type, step, overrides?)` function. Uses refs for `onEvent` and `controls` to keep callback identity stable. Composes event data from `store.getEventState()` + defaults (`error: null`, `scroll: null`) + step + type + overrides. Passes `controls` as second argument to `onEvent`.\n\nUsed by `usePropSync`, `useLifecycleEffect`, and `useScrollEffect` instead of calling `onEvent` directly.\n\n### usePropSync (`src/hooks/usePropSync.ts`)\n\nHandles external prop changes:\n\n- **Steps change**: Validates steps, calls `store.setSteps(steps)`. Fires `error` event if invalid.\n- **Run toggle**: `run=true` → `controls.start()`, `run=false` → `controls.stop()`\n\n### useLifecycleEffect (`src/hooks/useLifecycleEffect.ts`)\n\nFive separate effects drive the state machine:\n\n**Effect 1: Action tracking** (`deps: [action]`)\nTracks NEXT/PREV/SKIP/CLOSE actions in `lastAction` ref for enriching callbacks.\n\n**Effect 2: Target resolution** (`deps: [index, lifecycle, status, store]`)\nWhen RUNNING + step exists + lifecycle is INIT:\n1. Cleans up position data\n2. If step has `before` hook: sets `waiting=true`, fires `STEP_BEFORE_HOOK`, runs the async hook with AbortController + timeout. On resolve/reject/timeout → sets `lifecycle:READY, waiting:false`\n3. If no `before` hook: checks target visibility\n   - Target visible → `lifecycle:READY`\n   - `targetWaitTimeout=0` → `lifecycle:READY` (proceed without target)\n   - Otherwise → polls at 100ms intervals, sets `waiting=true`. On timeout → `lifecycle:READY`\n\n**Effect 3: Step presentation** (`deps: [lifecycle, store]`)\nWhen lifecycle reaches READY:\n- Target visible + from INIT → fires `STEP_BEFORE` event\n- Target visible → determines final lifecycle (BEACON or TOOLTIP via `hideBeacon()`), checks if scroll needed, sets `lifecycle:*_BEFORE, scrolling:willScroll`\n- Target not visible (RUNNING, not INIT/COMPLETE) → fires `TARGET_NOT_FOUND`, auto-advances in uncontrolled mode\n\n**Effect 4: Transitions + callbacks** (`deps: [lifecycle, positioned, scrolling, store]`)\n- `*_BEFORE` phase + `scrolling=false` → transitions to final BEACON/TOOLTIP\n- BEACON lifecycle → fires `beacon` event\n- TOOLTIP lifecycle → fires `tooltip` event\n- COMPLETE lifecycle (from TOOLTIP) → fires `step:after` event, runs `after` hook (fire-and-forget), fires `step:after_hook` event\n\n**Effect 5: Tour flow** (`deps: [action, index, lifecycle, size, status, store]`)\n- Action REPLAY + COMPLETE → writes `lifecycle:INIT` (works in both controlled and uncontrolled modes; bypasses the controlled-mode pause and the FINISHED branch on the last step to allow same-index re-entry)\n- No step + lifecycle INIT → sets FINISHED\n- Uncontrolled + COMPLETE + index < size → auto-advances to INIT (controlled mode pauses here, waiting for parent to update `stepIndex`)\n- COMPLETE + index >= size → sets FINISHED\n- Status → FINISHED/SKIPPED → fires `tour:end`, calls `controls.reset()`, clears lastAction\n- Status → RUNNING (from IDLE/READY/PAUSED) → fires `tour:start`\n- Action → STOP → fires `tour:status`, clears lastAction\n- Action → RESET → fires `tour:status`, clears lastAction\n\n### useScrollEffect (`src/hooks/useScrollEffect.ts`)\n\nScrolls to target when lifecycle reaches `*_before` phases and `positioned=true` and `scrolling=true`:\n\n- Custom scroll parent: first scrolls page to bring parent into view, then scrolls within parent\n- `adjustForPlacement()`: accounts for placement (top, left, right) to avoid tooltip being off-screen after scroll\n- Fires `scroll:start` and `scroll:end` events with `ScrollData` (initial, target, element, duration)\n- Sets `scrolling:false` on completion, triggering Effect 4's `*_BEFORE → BEACON/TOOLTIP` transition\n- Cancels previous scroll if new one starts\n\n## Step Processing\n\n### Step Merging (`src/modules/step.ts`)\n\n`getMergedStep(props, currentStep)` merges in two passes:\n\n**Pass 1 — step merge:**\n```\ndefaultStep → pick(props, basePropsFields) → currentStep\n```\n\n**Pass 2 — options merge:**\n```\ndefaultOptions → props.options → pick(currentStep, optionFieldNames)\n```\n\nFinal overrides applied after merge:\n- `locale`: deep-merged `defaultLocale → props.locale → step.locale`\n- `floatingOptions`: deep-merged `defaultFloatingOptions → props.floatingOptions → step.floatingOptions`\n- `spotlightPadding`: normalized to `{ top, right, bottom, left }` via `normalizeSpotlightPadding()`\n- `styles`: generated via `getStyles(step)`\n\nSharedProps fields picked from props: `arrowComponent, beaconComponent, floatingOptions, loaderComponent, locale, styles, tooltipComponent`\n\n### Step Hooks\n\nTwo hooks per step, defined as Options (globally or per-step):\n\n**`before(data: TourData) → Promise<void>`**: Blocks the tour at INIT phase. Sets `waiting=true` immediately; shows loader after `loaderDelay` (default 300ms). Capped by `beforeTimeout` (default 5000ms). On error/timeout, fires `error` event and proceeds.\n\n**`after(data: TourData) → void`**: Fire-and-forget after STEP_AFTER. Wrapped in try-catch — user code errors don't break the tour.\n\n### Target Resolution & Tracking\n\n**Resolution** (`getElement()` in `src/modules/dom.ts`):\n- CSS selector string → `document.querySelector()`\n- HTMLElement → used directly\n- React ref → `.current`\n- Function → called, returns element or null\n\n**Visibility** (`isElementVisible()`): walks DOM tree checking `display`/`visibility`.\n\n**Position tracking** (`src/hooks/useTargetPosition.ts`):\n- ResizeObserver on target element\n- Scroll/resize event listeners (passive)\n- Returns `{ top, left, width, height, isFixed }` with spotlightPadding applied\n- Disabled during scroll/waiting to avoid jitter\n\n## Rendering Pipeline\n\n```mermaid\nflowchart TD\n    J[\"Joyride (SSR guard)\"] --> UJ[useJoyride]\n    UJ --> TR[\"TourRenderer (fragment)\"]\n    TR --> S[\"Step\"]\n    TR --> P[\"Portal element\"]\n    S --> FT[useFocusTrap]\n    S --> F[Floater]\n    F --> P\n    P --> B[\"Beacon (BEACON phases)\"]\n    P --> T[\"Tooltip (TOOLTIP phases)\"]\n    P --> A[Arrow]\n    P --> L[\"Loader (waiting=true)\"]\n    P --> O[\"Overlay (SVG spotlight)\"]\n\n    style P fill:#f9f,stroke:#333\n```\n\nAll visible UI renders through Portal to the same portal element — `TourRenderer` and `Step` produce only React fragments, not DOM elements. Floater wraps beacon/tooltip/arrow in `<Portal>`, and TourRenderer wraps overlay/loader in `<Portal>`. Both target the same element from `usePortalElement`.\n\n### Key Components\n\n**Floater** (`src/components/Floater.tsx`):\n- Wraps all rendered content (beacon or tooltip + arrow) in `<Portal>` — nothing renders inline\n- Two `useFloating()` instances: tooltip (full middleware) and beacon (offset only)\n- Tooltip middleware: `offset` (includes spotlightPadding + arrow size), `flip`/`autoPlacement`, `shift`, `arrow`, plus custom middleware from `floatingOptions`\n- Center placement: custom middleware positions at viewport center, uses virtual reference element\n- Strategy: `fixed` when `isFixed=true` or target has fixed/sticky positioning, `absolute` otherwise\n- Reports position data to store via `setPositionData()` on each calculation\n\n**Overlay** (`src/components/Overlay.tsx`):\n- SVG-based with `fillRule=\"evenodd\"` for spotlight cutout\n- `useTargetPosition` tracks target rect via ResizeObserver + scroll/resize listeners\n- Two SVG paths: overlay path (full coverage with cutout) + cover path (fades out to reveal spotlight)\n- Visible during TOOLTIP/TOOLTIP_BEFORE (continuous) or TOOLTIP only (non-continuous); also during waiting\n- Hidden during BEACON/BEACON_BEFORE phases\n\n**Beacon** (`src/components/Beacon.tsx`):\n- Pulsing animation via CSS keyframes (injected once via style tag with existence check)\n- Default: button with inner/outer animated spans\n- Custom: `beaconComponent` prop receives render props\n- Auto-focuses on mount if `shouldFocus=true`\n\n**Tooltip** (`src/components/Tooltip/index.tsx`):\n- Generates button props (back, close, primary, skip) with handlers\n- Primary button: \"Next\" (continuous + not last), \"Last\" (last step), \"Close\" (non-continuous)\n- Delegates to DefaultTooltip (default) or `tooltipComponent` (custom)\n\n**DefaultTooltip** (`src/components/Tooltip/DefaultTooltip.tsx`):\n- Default layout: title → content → footer (skip, back, primary buttons) → close button\n- Footer hidden if `buttons` array has no footer buttons (back/primary/skip)\n\n**Arrow** (`src/components/Arrow.tsx`):\n- Default: SVG arrow element positioned by Floating UI middleware\n- Custom: `arrowComponent` prop receives `ArrowRenderProps`\n\n**Loader** (`src/components/Loader.tsx`):\n- Shown when `waiting=true` (target polling or `before` hook)\n- Custom: `loaderComponent` prop. Set to `null` to disable\n\n**Portal** (`src/components/Portal.tsx`):\n- `createPortal(children, element)` wrapper\n\n**usePortalElement** (`src/hooks/usePortalElement.ts`):\n- Creates `<div id=\"react-joyride-portal\">` in body, or uses provided `portalElement` (selector or HTMLElement)\n- Cleans up auto-created div on unmount\n\n## Appendix\n\n### DOM Utilities (`src/modules/dom.ts`)\n\n- `hasPosition(el, type?)`: Recursively checks for fixed/sticky positioning\n- `getScrollParent(element)`: Uses `scrollparent` library\n- `hasCustomScrollParent(element)`: Checks if scroll parent differs from document\n- `getScrollTo(element, offset)`: Calculates scroll position\n- `getScrollTargetToCenter(element)`: Centers element in viewport\n- `scrollTo(value, options)`: Smooth scroll via `scroll` library; returns `{ cancel, promise }`\n- `scrollDocument()`: Returns document scrolling element\n- `getDocumentHeight(median?)`: Takes max or median of 5 height measurements\n- `canUseDOM()`: SSR safety check\n\n### Focus Trap (`src/hooks/useFocusTrap.ts`)\n\n- Intercepts Tab key within tooltip element\n- Wraps focus forward/backward at tabbable element boundaries\n- Focuses initial selector (`[data-action=primary]`) on mount\n- Disabled when `step.disableFocusTrap=true`\n- Restores previous focus on cleanup\n\n### Styles (`src/styles.ts`)\n\n`getStyles(step)`:\n1. Merges step styles with generated defaults\n2. Adaptive width: `min(step.width, window.innerWidth - 30)`\n3. Generates style objects (beacon, buttons, tooltip, overlay, arrow, etc.)\n4. Returns merged with user overrides\n\nSee `src/defaults.ts` for current default values.\n\n### Types (`src/types/`)\n\n| File | Description |\n|------|-------------|\n| `common.ts` | Actions, Events, Lifecycle, Origin, Status, Placement, Locale, Options, Styles |\n| `components.ts` | ArrowRenderProps, BeaconRenderProps, LoaderRenderProps, TooltipRenderProps |\n| `events.ts` | EventData, EventHandler `(data, controls)`, TourData, ScrollData |\n| `floating.ts` | FloatingOptions, PositionData |\n| `props.ts` | Props, SharedProps, UseJoyrideReturn, StepFailure |\n| `state.ts` | Controls, State |\n| `step.ts` | Step, StepMerged, StepTarget, SelectorOrElement |\n| `utilities.ts` | PartialDeep, Simplify, SetRequired, ValueOf |\n\n### Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `@floating-ui/react-dom` | Tooltip/beacon positioning (dual instances) |\n| `@fastify/deepmerge` | Deep object merging (React element aware) |\n| `@gilbarbara/hooks` | useMemoDeepCompare, useMount, usePrevious, useUpdateEffect, useWindowSize |\n| `@gilbarbara/deep-equal` | Deep equality checking (store change detection) |\n| `@gilbarbara/types` | Utility types |\n| `scroll` / `scrollparent` | Smooth scrolling and scroll parent detection |\n| `react-innertext` | Extract text from React elements (button labels) |\n| `is-lite` | Type checking utilities |\n","isInternal":false,"tokens":5263,"sizeBytes":21357},{"name":"api-events-components.md","path":"skills/react-joyride/references/api-events-components.md","rawUrl":"https://raw.githubusercontent.com/gilbarbara/react-joyride/HEAD/skills/react-joyride/references/api-events-components.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Events, Constants, Custom Components\n\n## Constants\n\n### ACTIONS\n```typescript\nconst ACTIONS = {\n  INIT: 'init',\n  START: 'start',\n  STOP: 'stop',\n  RESET: 'reset',\n  PREV: 'prev',\n  NEXT: 'next',\n  GO: 'go',\n  CLOSE: 'close',\n  SKIP: 'skip',\n  UPDATE: 'update',\n  COMPLETE: 'complete',\n} as const;\n```\n\n### EVENTS\n```typescript\nconst EVENTS = {\n  TOUR_START: 'tour:start',\n  STEP_BEFORE_HOOK: 'step:before_hook',\n  STEP_BEFORE: 'step:before',\n  SCROLL_START: 'scroll:start',\n  SCROLL_END: 'scroll:end',\n  BEACON: 'beacon',\n  TOOLTIP: 'tooltip',\n  STEP_AFTER: 'step:after',\n  STEP_AFTER_HOOK: 'step:after_hook',\n  TOUR_END: 'tour:end',\n  TOUR_STATUS: 'tour:status',\n  TARGET_NOT_FOUND: 'error:target_not_found',\n  ERROR: 'error',\n} as const;\n```\n\n### LIFECYCLE\n```typescript\nconst LIFECYCLE = {\n  INIT: 'init',\n  READY: 'ready',\n  BEACON_BEFORE: 'beacon_before',\n  BEACON: 'beacon',\n  TOOLTIP_BEFORE: 'tooltip_before',\n  TOOLTIP: 'tooltip',\n  COMPLETE: 'complete',\n} as const;\n```\n\n### STATUS\n```typescript\nconst STATUS = {\n  IDLE: 'idle',\n  READY: 'ready',\n  WAITING: 'waiting',\n  RUNNING: 'running',\n  PAUSED: 'paused',\n  SKIPPED: 'skipped',\n  FINISHED: 'finished',\n} as const;\n```\n\n### ORIGIN\n```typescript\nconst ORIGIN = {\n  BUTTON_CLOSE: 'button_close',\n  BUTTON_SKIP: 'button_skip',\n  BUTTON_PRIMARY: 'button_primary',\n  KEYBOARD: 'keyboard',\n  OVERLAY: 'overlay',\n} as const;\n```\n\n## Event Types\n\n### EventData\n\nThe full payload passed to `onEvent` and event subscribers:\n\n```typescript\ntype EventData = TourData & {\n  error: Error | null;         // Only for 'error' events\n  scroll: ScrollData | null;   // Only for 'scroll:start' / 'scroll:end'\n  scrolling: boolean;\n  type: Events;                // The event type (discriminator)\n  waiting: boolean;\n}\n```\n\n### TourData\n\nBase payload (also passed to `before` and `after` hooks):\n\n```typescript\ninterface TourData {\n  action: Actions;\n  controlled: boolean;\n  index: number;\n  lifecycle: Lifecycle;\n  origin: Origin | null;\n  size: number;\n  status: Status;\n  step: StepMerged;\n}\n```\n\n### EventHandler\n\n```typescript\ntype EventHandler = (data: EventData, controls: Controls) => void;\n```\n\n### ScrollData\n\n```typescript\ntype ScrollData = {\n  duration: number;     // Scroll duration in ms\n  element: Element;     // Element being scrolled\n  initial: number;      // Scroll position before\n  target: number;       // Computed scroll destination\n}\n```\n\n## Event Flow (per step)\n\n```\ntour:start (once)\n  |\n  v\nstep:before_hook  (if before hook exists)\n  |\n  v\nstep:before  (target found, step about to render)\n  |\n  v\nscroll:start / scroll:end  (if scrolling needed)\n  |\n  v\nbeacon  (if beacon shown) OR tooltip  (if beacon skipped)\n  |\n  v  (user clicks beacon)\ntooltip\n  |\n  v  (user clicks next/prev/close/skip)\nstep:after\n  |\n  v\nstep:after_hook  (if after hook exists)\n  |\n  v  (next step or tour end)\ntour:end  (when finished/skipped)\n```\n\n## Custom Component Render Props\n\n### TooltipRenderProps\n\n```typescript\ntype TooltipRenderProps = {\n  // Tour state\n  continuous: boolean;\n  index: number;\n  isLastStep: boolean;\n  size: number;\n  step: StepMerged;\n\n  // Button props (spread on button elements)\n  backProps: {\n    'aria-label': string;\n    'data-action': string;\n    onClick: MouseEventHandler<HTMLElement>;\n    role: string;\n    title: string;\n  };\n  closeProps: { /* same shape */ };\n  primaryProps: { /* same shape */ };\n  skipProps: { /* same shape */ };\n\n  // Container props (spread on tooltip wrapper)\n  tooltipProps: {\n    'aria-modal': boolean;\n    role: string;  // 'dialog'\n  };\n\n  // Tour controls\n  controls: Controls;\n}\n```\n\n**Important**: Always spread `tooltipProps` on the root element and button props on their respective buttons. This ensures correct accessibility attributes and action handling.\n\n### BeaconRenderProps\n\n```typescript\ntype BeaconRenderProps = {\n  continuous: boolean;\n  index: number;\n  isLastStep: boolean;\n  size: number;\n  step: StepMerged;\n}\n```\n\nMust render a `<span>` since it's placed inside a `<button>` wrapper.\n\n### ArrowRenderProps\n\n```typescript\ntype ArrowRenderProps = {\n  base: number;          // Arrow base width\n  placement: Placement;  // Computed placement\n  size: number;          // Arrow height\n}\n```\n\n### LoaderRenderProps\n\n```typescript\ntype LoaderRenderProps = {\n  step: StepMerged;\n}\n```\n\nSet `loaderComponent={null}` on Props or Step to disable the loader entirely.\n\nDocs: https://v3.react-joyride.com/docs/custom-components\n","isInternal":false,"tokens":1210,"sizeBytes":4459},{"name":"api-props-options.md","path":"skills/react-joyride/references/api-props-options.md","rawUrl":"https://raw.githubusercontent.com/gilbarbara/react-joyride/HEAD/skills/react-joyride/references/api-props-options.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Props, Options, Locale, FloatingOptions, Styles\n\n## Props\n\n```typescript\ntype Props = SharedProps & {\n  continuous?: boolean;        // Sequential play with Next button (default: false)\n  debug?: boolean;             // Log to console (default: false)\n  initialStepIndex?: number;   // Start index for uncontrolled mode (default: 0)\n  nonce?: string;              // CSP nonce for inline styles\n  onEvent?: EventHandler;      // (data: EventData, controls: Controls) => void\n  options?: Partial<Options>;  // Default options for all steps\n  portalElement?: string | HTMLElement; // Render tooltip into this element\n  run?: boolean;               // Start/stop tour (default: false)\n  scrollToFirstStep?: boolean; // Scroll to first step on start (default: false)\n  stepIndex?: number;          // Controlled mode: manages step index externally\n  steps: Array<Step>;          // Required: tour steps\n}\n```\n\n## SharedProps\n\nInherited by both `Props` and `Step`:\n\n```typescript\ntype SharedProps = {\n  arrowComponent?: ElementType<ArrowRenderProps>;\n  beaconComponent?: ElementType<BeaconRenderProps>;\n  floatingOptions?: Partial<FloatingOptions>;\n  loaderComponent?: ElementType<LoaderRenderProps> | null; // null disables loader\n  locale?: Locale;\n  styles?: PartialDeep<Styles>;\n  tooltipComponent?: ElementType<TooltipRenderProps>;\n}\n```\n\n## Options\n\nAll fields can be set globally via `options` prop or per-step (step overrides global).\n\n### Appearance\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `backgroundColor` | `string` | `'#ffffff'` | Tooltip background |\n| `primaryColor` | `string` | `'#000000'` | Primary button and beacon |\n| `textColor` | `string` | `'#000000'` | Tooltip text |\n| `overlayColor` | `string` | `'#00000080'` | Overlay backdrop |\n| `arrowColor` | `string` | `'#ffffff'` | Arrow fill |\n| `width` | `string \\| number` | `380` | Tooltip width |\n| `zIndex` | `number` | `100` | z-index for overlay/tooltip |\n\n### Arrow\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `arrowBase` | `number` | `32` | Base width in pixels |\n| `arrowSize` | `number` | `16` | Height/depth in pixels |\n| `arrowSpacing` | `number` | `12` | Distance from tooltip edge |\n\n### Beacon\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `beaconSize` | `number` | `36` | Diameter in pixels |\n| `beaconTrigger` | `'click' \\| 'hover'` | `'click'` | How to open tooltip from beacon |\n\n### Overlay & Spotlight\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `hideOverlay` | `boolean` | `false` | Don't show overlay |\n| `blockTargetInteraction` | `boolean` | `false` | Block pointer events on target |\n| `spotlightRadius` | `number` | `4` | Border radius of cutout |\n| `spotlightPadding` | `number \\| SpotlightPadding` | `10` | Padding around target |\n\n```typescript\ninterface SpotlightPadding { top?: number; right?: number; bottom?: number; left?: number; }\n```\n\n### Buttons & Interactions\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `buttons` | `ButtonType[]` | `['back','close','primary']` | Buttons in tooltip |\n| `closeButtonAction` | `'close' \\| 'skip'` | `'close'` | Close button behavior |\n| `overlayClickAction` | `'close' \\| 'next' \\| false` | `'close'` | Overlay click behavior |\n| `dismissKeyAction` | `'close' \\| 'next' \\| false` | `'close'` | ESC key behavior |\n| `showProgress` | `boolean` | `false` | Show \"N of Total\" in tooltip |\n\n`ButtonType = 'back' | 'close' | 'primary' | 'skip'`\n\n### Scroll\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `skipBeacon` | `boolean` | `false` | Skip beacon, show tooltip directly |\n| `skipScroll` | `boolean` | `false` | Don't scroll to target |\n| `scrollDuration` | `number` | `300` | Scroll animation ms |\n| `scrollOffset` | `number` | `20` | Scroll distance from element |\n| `offset` | `number` | `10` | Distance between tooltip and spotlight |\n\n### Timing & Async\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `before` | `BeforeHook` | - | `(data: TourData) => Promise<void>` — runs before step |\n| `after` | `AfterHook` | - | `(data: TourData) => void` — runs after step (fire-and-forget) |\n| `beforeTimeout` | `number` | `5000` | Max wait for before hook (0 = no timeout) |\n| `targetWaitTimeout` | `number` | `1000` | Max wait for target to appear (0 = no waiting) |\n| `loaderDelay` | `number` | `300` | Delay before showing loader |\n| `disableFocusTrap` | `boolean` | `false` | Disable focus trap on tooltip |\n\n## Locale\n\n```typescript\ninterface Locale {\n  back?: ReactNode;              // 'Back'\n  close?: ReactNode;             // 'Close'\n  last?: ReactNode;              // 'Last'\n  next?: ReactNode;              // 'Next'\n  nextWithProgress?: ReactNode;  // 'Next ({current} of {total})'\n  open?: ReactNode;              // 'Open the dialog'\n  skip?: ReactNode;              // 'Skip'\n}\n```\n\n`{current}` and `{total}` are placeholder strings replaced at render time in `nextWithProgress`.\n\n## FloatingOptions\n\nControls `@floating-ui/react-dom` positioning:\n\n```typescript\ninterface FloatingOptions {\n  autoUpdate?: Partial<AutoUpdateOptions>;     // Auto-reposition options\n  beaconOptions?: { offset?: number };         // Beacon offset (default: -18)\n  flipOptions?: Partial<FlipOptions> | false;  // Flip middleware (false to disable)\n  hideArrow?: boolean;                         // Hide arrow (default: false)\n  middleware?: Array<Middleware>;               // Additional Floating UI middleware\n  onPosition?: (data: PositionData) => void;   // Position callback\n  shiftOptions?: Partial<ShiftOptions>;        // Shift middleware options\n  strategy?: 'fixed' | 'absolute';             // Auto-detected from step.isFixed\n}\n```\n\nDocs: https://v3.react-joyride.com/docs/props/floating-options\n\n## Styles\n\nOverride CSS for any UI element:\n\n```typescript\ninterface Styles {\n  arrow: CSSProperties;\n  beacon: CSSProperties;\n  beaconInner: CSSProperties;\n  beaconOuter: CSSProperties;\n  beaconWrapper: CSSProperties;\n  buttonBack: CSSProperties;\n  buttonClose: CSSProperties;\n  buttonPrimary: CSSProperties;\n  buttonSkip: CSSProperties;\n  floater: CSSProperties;\n  loader: CSSProperties;\n  overlay: CSSProperties;\n  tooltip: CSSProperties;\n  tooltipContainer: CSSProperties;\n  tooltipContent: CSSProperties;\n  tooltipFooter: CSSProperties;\n  tooltipFooterSpacer: CSSProperties;\n  tooltipTitle: CSSProperties;\n}\n```\n\nUse `PartialDeep<Styles>` — only override what you need.\n\nDocs: https://v3.react-joyride.com/docs/props/styles\n","isInternal":false,"tokens":1708,"sizeBytes":6658},{"name":"api-step-state-controls.md","path":"skills/react-joyride/references/api-step-state-controls.md","rawUrl":"https://raw.githubusercontent.com/gilbarbara/react-joyride/HEAD/skills/react-joyride/references/api-step-state-controls.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Step, State, Controls\n\n## Step\n\nA single step provided by the user. Extends `SharedProps` and `Partial<Options>`.\n\n```typescript\ntype Step = SharedProps & Partial<Options> & {\n  // Required\n  content: ReactNode;                           // Tooltip body\n  target: StepTarget;                           // Element to highlight\n\n  // Optional\n  id?: string;                                  // Unique identifier\n  title?: ReactNode;                            // Tooltip title\n  data?: any;                                   // Custom data (passed to callbacks)\n\n  // Positioning\n  placement?: Placement | 'auto' | 'center';   // Default: 'bottom'\n  beaconPlacement?: Placement;                  // Beacon-specific placement\n  isFixed?: boolean;                            // Force fixed positioning (default: false)\n\n  // Alternate targets\n  scrollTarget?: StepTarget;                    // Scroll to this instead of target\n  spotlightTarget?: StepTarget;                 // Highlight this instead of target\n}\n```\n\nAll `Options` fields (see api-props-options.md) are also valid on Step for per-step overrides.\n\n## StepTarget\n\nFour ways to specify a target element:\n\n```typescript\ntype StepTarget =\n  | string                              // CSS selector\n  | HTMLElement                         // Direct element reference\n  | RefObject<HTMLElement | null>       // React ref\n  | (() => HTMLElement | null);         // Function (re-evaluated each lifecycle)\n```\n\n## StepMerged\n\nThe normalized step after defaults are applied. All `Options` fields become required, plus:\n- `spotlightPadding: Required<SpotlightPadding>` (always `{ top, right, bottom, left }`)\n- `styles: Styles` (fully resolved)\n\nThis is what you receive in event callbacks and custom component props.\n\n## State\n\n```typescript\ntype State = {\n  action: Actions;         // What triggered the update (init, start, next, prev, etc.)\n  controlled: boolean;     // true when stepIndex prop is set\n  index: number;           // Current step index\n  lifecycle: Lifecycle;     // Step rendering phase\n  origin: Origin | null;   // UI element that triggered action\n  scrolling: boolean;      // Scroll animation in progress\n  size: number;            // Total number of steps\n  status: Status;          // Tour status (idle, ready, running, etc.)\n  waiting: boolean;        // Blocked on before hook or target polling\n}\n```\n\n## Controls\n\nAll 10 methods for programmatic tour control:\n\n```typescript\ntype Controls = {\n  close(origin?: Origin | null): void;\n  // Close the current step and advance to the next one.\n  // Optional origin for event tracking.\n\n  go(nextIndex: number): void;\n  // Jump to a specific step by index.\n  // Uncontrolled mode only — logs warning in controlled mode.\n\n  info(): State;\n  // Get the current tour state snapshot.\n\n  next(): void;\n  // Advance to the next step.\n\n  open(): void;\n  // Open the tooltip for the current step (skip beacon).\n\n  prev(): void;\n  // Go back to the previous step.\n\n  reset(restart?: boolean): void;\n  // Reset the tour to the beginning.\n  // If restart=true, also starts the tour.\n  // Uncontrolled mode only — logs warning in controlled mode.\n\n  skip(origin?: 'button_close' | 'button_skip'): void;\n  // Skip (end) the tour entirely. Sets status to SKIPPED.\n\n  start(nextIndex?: number): void;\n  // Start the tour. Optionally at a specific step index.\n\n  stop(advance?: boolean): void;\n  // Stop (pause) the tour. Sets status to PAUSED.\n  // If advance=true, moves to next step before stopping.\n}\n```\n\n## UseJoyrideReturn\n\n```typescript\ntype UseJoyrideReturn = {\n  controls: Controls;\n  // Programmatic tour control methods.\n\n  failures: StepFailure[];\n  // Steps that failed (target not found, before hook errors).\n  // Clears on start/reset.\n\n  on: (eventType: Events, handler: EventHandler) => () => void;\n  // Subscribe to a specific event type.\n  // Returns an unsubscribe function.\n\n  state: State;\n  // Current tour state (reactive, updates on changes).\n\n  step: StepMerged | null;\n  // Current merged step, or null if no step is active.\n\n  Tour: ReactElement | null;\n  // The tour React element. Render this in your JSX.\n}\n```\n\n## StepFailure\n\n```typescript\ninterface StepFailure {\n  reason: 'before_hook' | 'target_not_found';\n  step: StepMerged;\n}\n```\n\n## Placement\n\n```typescript\ntype Placement =\n  | 'top' | 'top-start' | 'top-end'\n  | 'bottom' | 'bottom-start' | 'bottom-end'\n  | 'left' | 'left-start' | 'left-end'\n  | 'right' | 'right-start' | 'right-end';\n\n// Step also accepts: 'auto' (Floating UI auto-placement) and 'center' (modal-style)\n```\n","isInternal":false,"tokens":1070,"sizeBytes":4582},{"name":"patterns.md","path":"skills/react-joyride/references/patterns.md","rawUrl":"https://raw.githubusercontent.com/gilbarbara/react-joyride/HEAD/skills/react-joyride/references/patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Common Patterns\n\n## Async Transitions with `before`/`after` Hooks (Preferred)\n\nThe library handles waiting for async operations. Use `before` hooks to prepare the UI before a step shows (open dropdowns, load data, trigger animations). The tour waits for the promise to resolve. Use `after` hooks for cleanup (fire-and-forget, does not block).\n\nThis is the **preferred approach** over controlled mode for most async scenarios.\n\n```tsx\nimport { useState } from 'react';\nimport { ACTIONS, STATUS, useJoyride } from 'react-joyride';\n\nfunction Dashboard() {\n  const [run, setRun] = useState(false);\n  const [isMenuOpen, setIsMenuOpen] = useState(false);\n\n  const { Tour } = useJoyride({\n    continuous: true,\n    run,\n    steps: [\n      {\n        target: '.nav-bar',\n        content: 'This is the navigation',\n        skipBeacon: true,\n      },\n      {\n        target: '.dropdown-item', // Element inside the dropdown\n        content: 'This dropdown item is important',\n        skipBeacon: true,\n        before: ({ action }) => {\n          // Open dropdown before this step — runs on both forward and back navigation\n          setIsMenuOpen(true);\n          return new Promise(resolve => setTimeout(resolve, 300));\n        },\n      },\n      {\n        target: '.main-content',\n        content: 'And here is the main content',\n        before: ({ action }) => {\n          // Close dropdown when moving to this step\n          setIsMenuOpen(false);\n          return new Promise(resolve => setTimeout(resolve, 300));\n        },\n      },\n    ],\n    onEvent: (data) => {\n      if ([STATUS.FINISHED, STATUS.SKIPPED].includes(data.status)) {\n        setRun(false);\n        setIsMenuOpen(false);\n      }\n    },\n  });\n\n  return (\n    <div>\n      {Tour}\n      <button onClick={() => setRun(true)}>Start Tour</button>\n      {/* ... */}\n    </div>\n  );\n}\n```\n\n## Before/After Hooks\n\n### Async data loading before a step\n\n```tsx\n{\n  target: '.user-profile',\n  content: 'Here is your profile data',\n  before: async () => {\n    await fetchUserProfile();\n    // Tour waits for this promise to resolve\n  },\n  beforeTimeout: 10000, // Allow up to 10s\n}\n```\n\n### Analytics tracking after a step\n\n```tsx\n{\n  target: '.feature',\n  content: 'Check out this feature',\n  after: (data) => {\n    // Fire-and-forget — does not block the tour\n    analytics.track('tour_step_completed', {\n      stepIndex: data.index,\n      action: data.action,\n    });\n  },\n}\n```\n\n### Delayed transitions (e.g., waiting for animation)\n\n```tsx\n{\n  target: '.sidebar',\n  content: 'The sidebar',\n  before: ({ action }) => {\n    // Only delay when navigating forward (not on back)\n    const ms = action === ACTIONS.PREV ? 0 : 300;\n    return new Promise(resolve => setTimeout(resolve, ms));\n  },\n}\n```\n\n## Custom Tooltip Component\n\n```tsx\nimport type { TooltipRenderProps } from 'react-joyride';\n\nfunction CustomTooltip({\n  backProps,\n  closeProps,\n  index,\n  isLastStep,\n  primaryProps,\n  size,\n  skipProps,\n  step,\n  tooltipProps,\n}: TooltipRenderProps) {\n  return (\n    <div\n      {...tooltipProps}\n      style={{\n        background: '#fff',\n        borderRadius: 8,\n        maxWidth: 400,\n        padding: 20,\n        width: step.width,\n      }}\n    >\n      {step.title && <h3 style={{ margin: '0 0 8px' }}>{step.title}</h3>}\n      <div>{step.content}</div>\n\n      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 16 }}>\n        {step.buttons.includes('skip') && !isLastStep && (\n          <button {...skipProps} type=\"button\">Skip</button>\n        )}\n        <div style={{ display: 'flex', gap: 8, marginLeft: 'auto' }}>\n          {index > 0 && (\n            <button {...backProps} type=\"button\">Back</button>\n          )}\n          <button {...primaryProps} type=\"button\">\n            {isLastStep ? 'Done' : `Next (${index + 1}/${size})`}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n// Usage:\n// <Joyride tooltipComponent={CustomTooltip} ... />\n// or in useJoyride: useJoyride({ tooltipComponent: CustomTooltip, ... })\n```\n\n## Event Subscription with `on()`\n\n```tsx\nfunction App() {\n  const { on, Tour } = useJoyride({\n    steps,\n    run: true,\n    continuous: true,\n  });\n\n  useEffect(() => {\n    const unsubs = [\n      on('tour:start', (data) => {\n        console.log('Tour started');\n      }),\n      on('tooltip', (data) => {\n        analytics.track('step_viewed', { index: data.index });\n      }),\n      on('tour:end', (data) => {\n        const wasSkipped = data.status === 'skipped';\n        analytics.track('tour_ended', { skipped: wasSkipped, lastStep: data.index });\n      }),\n    ];\n\n    return () => unsubs.forEach(unsub => unsub());\n  }, [on]);\n\n  return <div>{Tour}</div>;\n}\n```\n\n## Dynamic Steps\n\n```tsx\nfunction App() {\n  const [steps, setSteps] = useState<Step[]>([]);\n\n  useEffect(() => {\n    // Build steps based on feature flags, user role, etc.\n    const dynamicSteps: Step[] = [\n      { target: '.dashboard', content: 'Welcome to your dashboard' },\n    ];\n\n    if (user.isAdmin) {\n      dynamicSteps.push({\n        target: '.admin-panel',\n        content: 'Admin controls are here',\n      });\n    }\n\n    if (featureFlags.newSearch) {\n      dynamicSteps.push({\n        target: '.search-bar',\n        content: 'Try the new search',\n      });\n    }\n\n    setSteps(dynamicSteps);\n  }, [user, featureFlags]);\n\n  return (\n    <Joyride\n      run={steps.length > 0}\n      steps={steps}\n      continuous\n    />\n  );\n}\n```\n\n## React Ref Targets\n\n```tsx\nfunction App() {\n  const sidebarRef = useRef<HTMLDivElement>(null);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n\n  const steps: Step[] = [\n    { target: sidebarRef, content: 'Navigation sidebar' },\n    { target: buttonRef, content: 'Click here to create' },\n    { target: '.css-selector', content: 'Mix refs with selectors' },\n  ];\n\n  return (\n    <div>\n      <Joyride steps={steps} run continuous />\n      <div ref={sidebarRef}>Sidebar</div>\n      <button ref={buttonRef}>Create</button>\n      <span className=\"css-selector\">Other element</span>\n    </div>\n  );\n}\n```\n\n## Restart / Resume Tour\n\n```tsx\nfunction App() {\n  const [run, setRun] = useState(false);\n  const [initialStepIndex, setInitialStepIndex] = useState(0);\n\n  const handleStart = () => {\n    setInitialStepIndex(0);\n    setRun(true);\n  };\n\n  const handleResume = (fromStep: number) => {\n    setInitialStepIndex(fromStep);\n    setRun(true);\n  };\n\n  return (\n    <div>\n      <Joyride\n        run={run}\n        initialStepIndex={initialStepIndex}\n        steps={steps}\n        continuous\n        onEvent={(data) => {\n          if ([STATUS.FINISHED, STATUS.SKIPPED].includes(data.status)) {\n            setRun(false);\n          }\n        }}\n      />\n      <button onClick={handleStart}>Start Tour</button>\n      <button onClick={() => handleResume(3)}>Resume from Step 4</button>\n    </div>\n  );\n}\n```\n\n## Center Placement (Modal Style)\n\n```tsx\n{\n  target: 'body',\n  placement: 'center',\n  content: (\n    <div>\n      <h2>Welcome!</h2>\n      <p>This appears as a centered modal overlay.</p>\n    </div>\n  ),\n  // Center placement automatically hides beacon and arrow\n}\n```\n","isInternal":false,"tokens":1790,"sizeBytes":7109},{"name":"SKILL.md","path":"skills/react-joyride/SKILL.md","rawUrl":"https://raw.githubusercontent.com/gilbarbara/react-joyride/HEAD/skills/react-joyride/SKILL.md","title":"react-joyride","category":"anthropic-skill","format":"markdown","content":"---\nname: react-joyride\ndescription: >-\n  Guide for implementing, configuring, and debugging React Joyride v3 guided tours.\n  Use this skill whenever the user mentions joyride, guided tour, onboarding tour,\n  walkthrough, tooltip tour, step-by-step guide, product tour, or wants to highlight\n  UI elements sequentially. Also use when debugging tour issues like tooltips not\n  appearing, targets not found, or controlled mode problems. This skill covers the\n  useJoyride hook, Joyride component, step configuration, events, controls, custom\n  components, and styling.\nlicense: MIT\nmetadata:\n  author: gilbarbara\n  version: \"3.0.0\"\n---\n\n# React Joyride v3\n\nCreate guided tours in React apps. Two public APIs: the `useJoyride()` hook (recommended) and the `<Joyride>` component.\n\nOnline docs: https://v3.react-joyride.com\n\n## Quick Start\n\n### Using the hook (recommended)\n\n```tsx\nimport { useJoyride, STATUS, Status } from 'react-joyride';\n\nfunction App() {\n  const { Tour } = useJoyride({\n    continuous: true,\n    run: true,\n    steps: [\n      { target: '.my-element', content: 'This is the first step', title: 'Welcome' },\n      { target: '#sidebar', content: 'Navigate here', placement: 'right' },\n    ],\n    onEvent: (data) => {\n      if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n        // Tour ended\n      }\n    },\n  });\n\n  return <div>{Tour}{/* rest of app */}</div>;\n}\n```\n\n### Using the component\n\n```tsx\nimport { Joyride, STATUS, Status } from 'react-joyride';\n\nfunction App() {\n  return (\n    <Joyride\n      continuous\n      run={true}\n      steps={[\n        { target: '.my-element', content: 'First step' },\n        { target: '#sidebar', content: 'Second step' },\n      ]}\n      onEvent={(data) => {\n        if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n          // Tour ended\n        }\n      }}\n    />\n  );\n}\n```\n\nThe hook returns `{ controls, failures, on, state, step, Tour }`. Render `Tour` in your JSX.\n\nDocs: https://v3.react-joyride.com/docs/getting-started\n\n## Core Concepts\n\nThe tour has two state dimensions:\n\n**Tour Status**: `idle -> ready -> waiting -> running <-> paused -> finished | skipped`\n- `idle`: No steps loaded\n- `ready`: Steps loaded, waiting for `run: true`\n- `waiting`: `run=true` but steps loading async (transitions to running when steps arrive)\n- `running`: Tour active\n- `paused`: Tour paused (controlled mode at COMPLETE, or `stop()` called)\n- `finished` / `skipped`: Tour ended\n\n**Step Lifecycle** (per step): `init -> ready -> beacon_before -> beacon -> tooltip_before -> tooltip -> complete`\n- `*_before` phases: scrolling and positioning happen here\n- `beacon`: Pulsing indicator shown (skipped when `continuous` + navigating, `skipBeacon`, or `placement: 'center'`)\n- `tooltip`: The tooltip is visible and interactive\n\nDocs: https://v3.react-joyride.com/docs/how-it-works\n\n## Step Configuration\n\nEach step requires `target` and `content`. All other fields are optional.\n\n```tsx\n{\n  target: '.my-element',       // CSS selector, HTMLElement, React ref, or () => HTMLElement\n  content: 'Step body text',   // ReactNode\n  title: 'Optional title',    // ReactNode\n  placement: 'bottom',        // Default. Also: top, left, right, *-start, *-end, auto, center\n  id: 'unique-id',            // Optional identifier\n  data: { custom: 'data' },   // Attached to event callbacks\n}\n```\n\n### Target types\n\n```tsx\n// CSS selector\n{ target: '.sidebar-nav' }\n// HTMLElement\n{ target: document.getElementById('my-el') }\n// React ref\nconst ref = useRef(null);\n{ target: ref }\n// Function (evaluated each lifecycle)\n{ target: () => document.querySelector('.dynamic-element') }\n```\n\n### Common step options (override per-step)\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `placement` | `'bottom'` | Tooltip position. Use `'center'` for modal-style (requires `target: 'body'`) |\n| `skipBeacon` | `false` | Skip beacon, show tooltip directly |\n| `buttons` | `['back','close','primary']` | Buttons in tooltip. Add `'skip'` for skip button |\n| `hideOverlay` | `false` | Don't show dark overlay |\n| `blockTargetInteraction` | `false` | Block clicks on highlighted element |\n| `before` | - | `(data) => Promise<void>` — async hook before step shows |\n| `after` | - | `(data) => void` — fire-and-forget hook after step completes |\n| `skipScroll` | `false` | Don't scroll to target |\n| `scrollTarget` | - | Scroll to this element instead of `target` |\n| `spotlightTarget` | - | Highlight this element instead of `target` |\n| `spotlightPadding` | `10` | Padding around spotlight. Number or `{ top, right, bottom, left }` |\n| `targetWaitTimeout` | `1000` | ms to wait for target to appear. `0` = no waiting |\n| `beforeTimeout` | `5000` | ms to wait for `before` hook. `0` = no timeout |\n\nAll `Options` fields can be set globally via `options` prop or per-step. Per-step values override global.\n\nDocs: https://v3.react-joyride.com/docs/step | https://v3.react-joyride.com/docs/props/options\n\n## Uncontrolled vs Controlled\n\n### Uncontrolled (default — strongly preferred)\n\nThe tour manages step navigation internally. This is the right choice for most use cases.\n\n**The library handles async transitions for you.** If a step needs to wait for a UI change (dropdown opening, data loading, animation), use `before` hooks — the tour waits for the promise to resolve before showing the step. If a target element isn't in the DOM yet, `targetWaitTimeout` (default: 1000ms) handles polling for it. You do NOT need controlled mode for these cases.\n\n```tsx\nconst { Tour } = useJoyride({\n  continuous: true,\n  run: isRunning,\n  steps: [\n    { target: '.nav', content: 'Navigation' },\n    {\n      target: '.dropdown-item',\n      content: 'Inside the dropdown',\n      before: () => {\n        // Open dropdown and wait for animation — tour waits automatically\n        openDropdown();\n        return new Promise(resolve => setTimeout(resolve, 300));\n      },\n      after: () => closeDropdown(), // Clean up after step (fire-and-forget)\n    },\n    { target: '.main-content', content: 'Main content' },\n  ],\n  onEvent: (data) => {\n    if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n      setIsRunning(false);\n    }\n  },\n});\n```\n\n### Controlled (with `stepIndex`) — use sparingly\n\nOnly use controlled mode when the parent genuinely needs to manage the step index externally (e.g., syncing with URL params, external state machines, or complex multi-component coordination that `before`/`after` hooks can't handle).\n\n```tsx\nconst [stepIndex, setStepIndex] = useState(0);\nconst [run, setRun] = useState(true);\n\nconst { Tour } = useJoyride({\n  continuous: true,\n  run,\n  stepIndex,  // This makes it controlled\n  steps,\n  onEvent: (data) => {\n    const { action, index, status, type } = data;\n\n    if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(status)) {\n      setRun(false);\n      return;\n    }\n\n    if (type === 'step:after' || type === 'error:target_not_found') {\n      setStepIndex(index + (action === 'prev' ? -1 : 1));\n    }\n  },\n});\n```\n\n**Controlled mode rules:**\n- `go()` and `reset()` are disabled (logged warning)\n- You must update `stepIndex` in response to events\n- The tour pauses at COMPLETE — you must advance it\n- Prefer uncontrolled mode with `before`/`after` hooks unless you have a strong reason for external index management\n\n## Event System\n\n### `onEvent` callback\n\n```tsx\nonEvent: (data: EventData, controls: Controls) => void\n```\n\nThe `data` object contains the full tour state plus event-specific fields. The `controls` object lets you programmatically control the tour.\n\n### Event types (in order per step)\n\n| Event | When |\n|-------|------|\n| `tour:start` | Tour begins |\n| `step:before_hook` | `before` hook is called |\n| `step:before` | Target found, step about to render |\n| `scroll:start` | Scrolling to target |\n| `scroll:end` | Scroll complete |\n| `beacon` | Beacon shown |\n| `tooltip` | Tooltip shown |\n| `step:after` | User navigated (next/prev/close/skip) |\n| `step:after_hook` | `after` hook called |\n| `tour:end` | Tour finished or skipped |\n| `tour:status` | Status changed (on stop/reset) |\n| `error:target_not_found` | Target element not found |\n| `error` | Generic error |\n\n### Event subscription with `on()`\n\n```tsx\nconst { on, Tour } = useJoyride({ ... });\n\nuseEffect(() => {\n  const unsubscribe = on('tooltip', (data, controls) => {\n    analytics.track('tour_step_viewed', { step: data.index });\n  });\n  return unsubscribe;\n}, [on]);\n```\n\nDocs: https://v3.react-joyride.com/docs/events\n\n## Controls\n\nAvailable via `useJoyride()` return value or `onEvent` second argument:\n\n| Method | Description |\n|--------|-------------|\n| `next()` | Advance to next step |\n| `prev()` | Go to previous step |\n| `close(origin?)` | Close current step, advance |\n| `skip(origin?)` | Skip the tour entirely |\n| `start(index?)` | Start the tour |\n| `stop(advance?)` | Stop (pause) the tour |\n| `go(index)` | Jump to step (uncontrolled only) |\n| `reset(restart?)` | Reset tour (uncontrolled only) |\n| `open()` | Open tooltip for current step |\n| `info()` | Get current state |\n\nDocs: https://v3.react-joyride.com/docs/hook\n\n## Styling & Theming\n\nThree layers of customization (from simple to full control):\n\n### 1. Color options (simplest)\n\n```tsx\noptions: {\n  primaryColor: '#1976d2',      // Buttons and beacon\n  backgroundColor: '#1a1a2e',   // Tooltip background\n  textColor: '#ffffff',         // Tooltip text\n  overlayColor: 'rgba(0,0,0,0.7)', // Overlay backdrop\n  arrowColor: '#1a1a2e',        // Arrow (match background)\n}\n```\n\n### 2. Styles override\n\n```tsx\nstyles: {\n  tooltip: { borderRadius: 12 },\n  buttonPrimary: { backgroundColor: '#1976d2' },\n  buttonBack: { color: '#666' },\n  spotlight: { borderRadius: 8 },\n}\n```\n\nStyle keys: `arrow`, `beacon`, `beaconInner`, `beaconOuter`, `beaconWrapper`, `buttonBack`, `buttonClose`, `buttonPrimary`, `buttonSkip`, `floater`, `loader`, `overlay`, `tooltip`, `tooltipContainer`, `tooltipContent`, `tooltipFooter`, `tooltipFooterSpacer`, `tooltipTitle`\n\n### 3. Custom components (full control)\n\nSee next section.\n\nDocs: https://v3.react-joyride.com/docs/props/styles\n\n## Custom Components\n\nReplace any UI component via props. Each receives render props with step data and button handlers.\n\n### Custom Tooltip\n\n```tsx\nimport type { TooltipRenderProps } from 'react-joyride';\n\nfunction MyTooltip({ backProps, index, primaryProps, size, skipProps, step, tooltipProps }: TooltipRenderProps) {\n  return (\n    <div {...tooltipProps} style={{ background: '#fff', padding: 16, borderRadius: 8, width: step.width }}>\n      {step.title && <h3>{step.title}</h3>}\n      <div>{step.content}</div>\n      <div>\n        {index > 0 && <button {...backProps}>Back</button>}\n        <button {...primaryProps}>Next</button>\n      </div>\n    </div>\n  );\n}\n\n// Usage\n<Joyride tooltipComponent={MyTooltip} ... />\n```\n\n**Important**: Spread `tooltipProps` on the container (sets `role=\"dialog\"` and `aria-modal`). Spread button props (`backProps`, `primaryProps`, `closeProps`, `skipProps`) on buttons for correct action handling.\n\n### Custom Beacon\n\nMust render a `<span>` (placed inside a `<button>` wrapper). Receives `BeaconRenderProps`: `{ continuous, index, isLastStep, size, step }`.\n\n### Custom Arrow\n\nReceives `ArrowRenderProps`: `{ base, placement, size }`.\n\n### Custom Loader\n\nReceives `LoaderRenderProps`: `{ step }`. Set to `null` to disable the loader.\n\nDocs: https://v3.react-joyride.com/docs/custom-components\n\n## Problem → Solution Guide\n\n| I need to... | Use |\n|---|---|\n| Wait for async UI changes between steps (dropdown, animation, data load) | `before` hook returning a Promise — not controlled mode |\n| Control step navigation externally (URL sync, external state machine) | Controlled mode with `stepIndex` — but try `before`/`after` hooks first |\n| Track which steps failed (target missing, hook error) | `failures` array from `useJoyride()` return |\n| Listen to specific events without `onEvent` switch | `on('event:type', handler)` from `useJoyride()` return |\n| Show a centered modal-style step | `target: 'body'` + `placement: 'center'` |\n\n## Common Gotchas & Debugging\n\n### Use `debug: true` first\n\nThe `debug` prop is the most powerful troubleshooting tool. It logs detailed lifecycle transitions, state changes, and event emissions to the console. Always start here when something isn't working.\n\n```tsx\n<Joyride debug={true} ... />\n// or\nuseJoyride({ debug: true, ... })\n```\n\nThe console output shows exactly which lifecycle phase the tour reaches, what actions are firing, and where it gets stuck.\n\n### Tour not starting\n- Check `run={true}` is set\n- Verify `steps` array is not empty and steps have valid `target` + `content`\n- For SSR: use `<Joyride>` component (auto-guards DOM access) or check `typeof window !== 'undefined'`\n\n### Target not found\n- Test selector in console: `document.querySelector('.your-selector')`\n- Element must be visible (not `display: none`, `visibility: hidden`, or zero dimensions)\n- If element mounts later, increase `targetWaitTimeout` (default: 1000ms)\n- Set `targetWaitTimeout: 0` to skip waiting entirely\n- In uncontrolled mode, missing targets auto-advance; in controlled mode, handle `error:target_not_found` event\n\n### Tooltip never appears / overlay flashes\n- Add `debug: true` and check the console to see which lifecycle phase is reached\n- Verify the target element is in the viewport or scrollable\n- Check for CSS `overflow: hidden` on ancestors clipping the target\n- If using portals or modals, the target may not be accessible\n\n### Controlled mode stuck\n- First question: do you actually need controlled mode? Most async needs are solved with `before`/`after` hooks in uncontrolled mode\n- If controlled: you MUST update `stepIndex` in your `onEvent` handler when `type === 'step:after'`\n- Handle both forward (`action !== 'prev'`) and backward (`action === 'prev'`) navigation\n- Also handle `error:target_not_found` to skip broken steps\n- `go()` and `reset()` don't work in controlled mode\n\n### Before hook timeout\n- Default `beforeTimeout` is 5000ms — increase if your async operation takes longer\n- Set `beforeTimeout: 0` for no timeout\n- The loader appears after `loaderDelay` (300ms) while waiting\n\n### Scroll issues\n- Use `scrollTarget` to scroll to a different element than the tooltip target\n- Adjust `scrollOffset` (default: 20px) for headers or fixed elements\n- Set `skipScroll: true` to disable auto-scrolling for a step\n- `scrollToFirstStep: false` by default — set to `true` if first step is off-screen\n\n### Center placement\n- Use `placement: 'center'` with `target: 'body'` for modal-style centered tooltips\n- Center placement automatically hides the beacon and arrow\n\n### Imports\n\n```tsx\n// Named exports only (no default export in v3)\nimport { Joyride, useJoyride } from 'react-joyride';\n\n// Constants for type-safe comparisons\nimport { ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS } from 'react-joyride';\n\n// Types\nimport type { Step, Props, EventData, Controls, TooltipRenderProps } from 'react-joyride';\n```\n\nDocs: https://v3.react-joyride.com/docs/exports\n\n## Reference Files\n\nRead these for complete API details:\n\n- `references/api-props-options.md` — Full Props, Options (all 30+ fields with defaults), Locale, FloatingOptions, Styles types\n- `references/api-step-state-controls.md` — Step, StepMerged, StepTarget, State, Controls (all 10 methods), UseJoyrideReturn, StepFailure\n- `references/api-events-components.md` — All 13 event types, ACTIONS/LIFECYCLE/STATUS/ORIGIN constants, EventData, custom component render props\n- `references/patterns.md` — Complete working examples: controlled mode, before/after hooks, custom tooltip, event subscription, dynamic steps\n","frontmatter":{"name":"react-joyride","description":"Guide for implementing, configuring, and debugging React Joyride v3 guided tours. Use this skill whenever the user mentions joyride, guided tour, onboarding tour, walkthrough, tooltip tour, step-by-step guide, product tour, or wants to highlight UI elements sequentially. Also use when debugging tour issues like tooltips not appearing, targets not found, or controlled mode problems. This skill covers the useJoyride hook, Joyride component, step configuration, events, controls, custom components, and styling.","license":"MIT","metadata":{"author":"gilbarbara","version":"3.0.0"}},"isInternal":false,"tokens":4026,"sizeBytes":15777},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/gilbarbara/react-joyride/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\n## Overview\n\nReact Joyride is a React library for creating guided tours (tooltips, walkthroughs, onboarding). Supports React 16.8-19 and SSR.\n\nFor deep architecture details, see `docs/architecture.md`.\n\n## Commands\n\nPackage manager: **pnpm**\n\n```bash\npnpm lint              # ESLint with --fix on src/, test/, and e2e/\npnpm typecheck         # tsc using test/tsconfig.json\npnpm test              # Vitest (run once)\npnpm test:coverage     # With coverage\npnpm test:watch        # Watch mode\npnpm e2e               # Playwright (chromium, firefox, webkit)\npnpm e2e:chromium      # Chromium only\npnpm website           # Website at localhost:3000\npnpm website:build     # Build website for production\npnpm website:start     # Serve built website locally\npnpm website:serve     # Build + start website\npnpm build             # tsdown\npnpm check             # lint → typecheck → test:coverage\npnpm validate          # check → build → size → typevalidation\n```\n\nSingle test: `pnpm test modules/store` | Single e2e: `pnpm e2e:chromium standard`\n\n## How It Works\n\nSingleton `Store` class (ref-based) manages frozen state snapshots. React subscribes via `useSyncExternalStore`.\n\nTwo public APIs: `<Joyride>` component and `useJoyride()` hook (returns `{ controls, failures, on, state, step, Tour }`).\n\n**State machine** has two dimensions: tour `Status` (`idle → ready → waiting → running ↔ paused → finished/skipped`) and step `Lifecycle` (`init → ready → beacon_before → beacon → tooltip_before → tooltip → complete`). See architecture.md for full details.\n\n**Event system**: Single `onEvent(data, controls)` callback receives discriminated events and tour controls: `tour:start`, `step:before_hook`, `step:before`, `scroll:start`, `scroll:end`, `beacon`, `tooltip`, `step:after`, `step:after_hook`, `tour:end`, `tour:status`, `error:target_not_found`, `error`.\n\n**Controlled** (with `stepIndex` prop): Tour pauses at COMPLETE; parent manages index via `onEvent`. **Uncontrolled**: Store manages index internally; supports `initialStepIndex`.\n\n## Key Files\n\n| File | Role |\n|------|------|\n| `src/index.tsx` | SSR-safe wrapper, exports types and literals |\n| `src/hooks/useJoyride.tsx` | Public hook: orchestrates tour, returns controls/state/Tour |\n| `src/hooks/useTourEngine.ts` | Store init, subscriptions, delegates to sub-hooks |\n| `src/hooks/useControls.ts` | Tour control methods (next, prev, start, stop, etc.) |\n| `src/hooks/useEventEmitter.ts` | Centralized event emission with controls |\n| `src/hooks/usePropSync.ts` | Syncs run/steps props to store |\n| `src/hooks/useLifecycleEffect.ts` | 5 effects: action tracking, target resolution, presentation, transitions, tour flow |\n| `src/hooks/useScrollEffect.ts` | Scroll-to-target with placement adjustments |\n| `src/hooks/usePortalElement.ts` | Portal DOM element management |\n| `src/hooks/useTargetPosition.ts` | Target rect tracking via ResizeObserver |\n| `src/hooks/useFocusTrap.ts` | Focus trap for tooltip accessibility |\n| `src/modules/store.ts` | State machine + frozen snapshots |\n| `src/modules/dom.ts` | Element lookup, scroll, visibility |\n| `src/modules/step.ts` | Step merging + validation |\n| `src/modules/helpers.tsx` | Utilities (deepMerge, mergeProps, logDebug) |\n| `src/modules/changes.ts` | State transition detection (treeChanges) |\n| `src/modules/svg.ts` | SVG path generation for overlay/spotlight |\n| `src/defaults.ts` | Default props, locale, options |\n| `src/styles.ts` | Style generation from step |\n| `src/literals/index.ts` | Constants (ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS) |\n\n## Components\n\n| Component | File | Purpose |\n|-----------|------|---------|\n| TourRenderer | `src/components/TourRenderer.tsx` | Conditional rendering, keyboard handler (ESC) |\n| Step | `src/components/Step.tsx` | Validates step + target, renders Floater |\n| Floater | `src/components/Floater.tsx` | Dual Floating UI instances (beacon + tooltip), arrow |\n| Beacon | `src/components/Beacon.tsx` | Animated pulsing indicator |\n| Tooltip | `src/components/Tooltip/index.tsx` | Button handlers, custom component support |\n| DefaultTooltip | `src/components/Tooltip/DefaultTooltip.tsx` | Default tooltip layout |\n| CloseButton | `src/components/Tooltip/CloseButton.tsx` | SVG close icon |\n| Arrow | `src/components/Arrow.tsx` | Tooltip arrow, customizable via `arrowComponent` |\n| Overlay | `src/components/Overlay.tsx` | SVG-based spotlight with path cutout |\n| Loader | `src/components/Loader.tsx` | Loading indicator during target wait/before hooks |\n| Portal | `src/components/Portal.tsx` | React portal wrapper |\n\n## Types\n\nSplit into 8 domain files under `src/types/`: `common`, `components`, `events`, `floating`, `props`, `state`, `step`, `utilities`. Re-exported from `src/types/index.ts`.\n\n## Path Alias\n\n`~/*` maps to `src/*` (tsconfig.json; vitest via native `resolve.tsconfigPaths`; playwright uses website webServer).\n\n## Test Structure\n\n- **Unit tests** (Vitest + jsdom): `test/modules/`, `test/components/`, `test/hooks/`, `test/tours/`\n- **E2E tests** (Playwright): `e2e/` with screenshot snapshots per browser\n- **Fixtures**: `test/__fixtures__/` — Demo components used by both suites\n\nCoverage thresholds: 90% statements, 80% branches, 90% functions, 90% lines.\n\n## Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `@floating-ui/react-dom` | Tooltip/beacon positioning |\n| `@fastify/deepmerge` | Deep object merging (React element aware) |\n| `@gilbarbara/hooks` | useMemoDeepCompare, useMount, usePrevious, useUpdateEffect, useWindowSize |\n| `@gilbarbara/deep-equal` | Deep equality checking |\n| `scroll` / `scrollparent` | Smooth scrolling and scroll parent detection |\n| `react-innertext` | Extract text from React elements |\n| `is-lite` | Type checking utilities |\n\n## Website\n\nNext.js 16 documentation and demo site in `website/`. Imports library via local path (`\"react-joyride\": \"../\"`).\n\n**Stack**: HeroUI v2, Tailwind CSS v4, MDX (rehype-pretty-code + Shiki), next-themes, DocSearch.\n\n**Bundler**: `dev` runs on Turbopack; `build` stays on `--webpack` (`dev:webpack` is the fallback). `next build --turbopack` fails: `@next/mdx`'s Turbopack path never applies the `isServer` vendored-RSC-React alias (its `webpack()` hook does), so server-evaluated modules get the `react-server` React and any interactive HeroUI component's `@react-aria/ssr` `createContext` throws app-wide. The webpack alias in `next.config.ts` masks this; Turbopack has no server-only alias equivalent (a global `resolveAlias` pulls server React into the client bundle). Upstream: nextui-org/nextui#3967, shuding/nextra#4703.\n\n**Documentation** (`website/src/app/docs/`): MDX pages covering getting-started, new-in-v3, how-it-works, hook, props (options, styles, floating-options), step, events, custom-components, exports, accessibility, migration.\n\n**Demos** (`website/src/app/demos/`): 8 interactive examples — overview, controlled, custom-components, scroll, modal, carousel, chat, multi-route.\n\n**Key patterns**:\n- `ConfigContext` — centralized tour configuration for demos\n- `website/src/components/` — 33 components (ConfigPanel, custom tour components, code blocks, navigation)\n- `website/src/config/` — sidebar nav, default values, hero content\n\n## Visual Customization\n- Visual/cosmetic options (colors, borders, sizes) belong in `styles.*`, not as new Options fields.\n- Before adding a new option for appearance, check if it fits an existing or new `styles` key.\n","isInternal":false,"tokens":1888,"sizeBytes":7502}],"systemPromptSnippet":"<yakaai_skills repo=\"gilbarbara/react-joyride\">\n<!-- File: docs/architecture.md (Tokens: ~5263 | Category: project-spec) -->\n# React Joyride Architecture\n\n## Data Flow\n\n```mermaid\nflowchart TD\n    Props --> useJoyride\n    useJoyride --> useTourEngine\n    useTourEngine --> Store\n    Store --> useSyncExternalStore\n    useSyncExternalStore --> TourRenderer\n    TourRenderer --> Step\n    TourRenderer --> Portal\n    Step --> Floater --> Portal\n    Portal --> Overlay & Loader & Beacon & Tooltip\n```\n\nTwo public APIs:\n- `<Joyride>` component — SSR-safe wrapper that calls `useJoyride` internally\n- `useJoyride(props)` hook — returns `{ controls, failures, on, state, step, Tour }`\n\n## State Machine\n\nTwo dimensions drive the tour:\n\n### Tour Status (overall)\n\n```mermaid\nstateDiagram-v2\n    [*] --> idle\n    idle --> ready : steps loaded\n    ready --> waiting : run=true, no steps\n    ready --> running : run=true + steps\n    waiting --> running : size > 0\n    running --> paused : stop()\n    paused --> running : start()\n    running --> finished : last step complete\n    running --> skipped : skip()\n    finished --> ready : reset()\n    skipped --> ready : reset()\n    paused --> ready : reset()\n```\n\n- `idle`: No steps loaded\n- `ready`: Steps loaded, waiting for `run=true`\n- `waiting`: `run=true` but steps loading async (transitions to RUNNING when size > 0)\n- `running`: Tour active\n- `paused`: Stopped mid-tour via `stop()` (can resume with `start()`)\n- `finished`: Tour completed normally\n- `skipped`: Tour ended via `skip()`\n\n### Step Lifecycle (per step)\n\n```mermaid\nstateDiagram-v2\n    [*] --> init\n    init --> ready : target found / before resolved\n    ready --> beacon_before : beacon path\n    ready --> tooltip_before : skip beacon\n    beacon_before --> beacon : scroll done\n    beacon --> tooltip_before : user click\n    tooltip_before --> tooltip : scroll done\n    tooltip --> complete : user action\n    complete --> [*]\n```\n\n- `init`: Step starting, target check pending, `before` hook runs here\n- `ready`: Target found and visible, preparing render\n- `beacon_before`: Beacon positioned, waiting for scroll to complete\n- `beacon`: Showing pulsing beacon\n- `tooltip_before`: Tooltip positioned, waiting for scroll to complete\n- `tooltip`: Showing tooltip content\n- `complete`: Step done, transitioning to next\n\nBeacon is skipped (goes straight to `tooltip_before`) when:\n- `skipBeacon = true`\n- `step.placement = 'center'`\n- `continuous` mode AND action is NEXT or PREV\n\nThe `*_before` phases exist to hold rendering while scroll animation completes. The scroll effect watches for `positioned=true` during these phases, scrolls, then sets `scrolling=false`, which triggers the transition to the final `beacon`/`tooltip` phase.\n\n### Complete State Transition Sequence\n\nA full multi-step tour from mount to finish:\n\n```\nmount: run=true, steps=[...]\n  → start() → status:running, lifecycle:init\n  → tour:start event\n  → [before hook runs if defined → step:before_hook event → waiting:true → loader shown]\n  → lifecycle: init→ready (target check / before hook resolved)\n  → step:before event (action: lastAction ?? update)\n  → lifecycle: ready → *_before (beacon_before or tooltip_before)\n  → scrolling:true, positioned via Floating UI → scroll animation\n  → scroll:start event\n  → scroll:end event → scrolling:false\n  → lifecycle: *_before → beacon or tooltip\n  → beacon event\n  → User clicks beacon → lifecycle: beacon → tooltip_before → tooltip\n  → tooltip event\n  → Focus trap activated\n  → User clicks \"Next\" → controls.next() → lifecycle:complete, index+1\n  → step:after event (reports previous index)\n  → [after hook runs if defined → step:after_hook event]\n  → lifecycle: complete → init (auto-advance in uncontrolled; controlled waits for parent to update stepIndex)\n  → ... repeats for each step ...\n  → Last step: index >= size → status:finished\n  → tour:end event (reports last step)\n  → controls.reset() → status:ready, index:0\n```\n\n## Controlled vs Uncontrolled\n\n**Uncontrolled** (default): Store manages index internally. `next()`/`prev()`/`go()`/`reset()` work. Auto-advances past missing targets. Supports `initialStepIndex` for starting at a specific step.\n\n**Controlled** (with `stepIndex` prop): Tour pauses at each step's COMPLETE lifecycle phase instead of auto-advancing. Parent manages the step index via `onEvent` callbacks. `go()` and `reset()` are disabled in controlled mode.\n\n## Store (`src/modules/store.ts`)\n\nClass managing immutable state via plain objects. Snapshots are `Object.freeze()`d. Created once per component instance via `useRef`. Subscriptions drive React re-renders via `useSyncExternalStore`.\n\n### State Shape\n\n```typescript\n{\n  action: Actions        // What triggered the change\n  controlled: boolean    // true if controlled prop is set\n  index: number          // Current step index\n  lifecycle: Lifecycle   // Step lifecycle phase\n  origin: Origin | null  // What triggered the action (button_close, button_skip, button_primary, keyboard, overlay)\n  positioned: boolean    // Whether floating element has been positioned (internal only)\n  scrolling: boolean     // Whether scroll animation is in progress\n  size: number           // Total steps count\n  status: Status         // Tour status\n  waiting: boolean       // Waiting for before hook or target polling\n}\n```\n\n`positioned` is internal — stripped from public `State` type via `omit(snapshot, 'positioned')`.\n\n### Store Methods\n\n| Method | Description |\n|--------|-------------|\n| `updateState(patch, forceIndex?)` | Merge patch, freeze snapshot, notify listeners. Controlled mode ignores `index` unless `forceIndex=true` |\n| `subscribe(listener)` | Returns unsubscribe function. Used by `useSyncExternalStore` |\n| `getSnapshot()` / `getServerSnapshot()` | Frozen state for `useSyncExternalStore` |\n| `getState()` | Public state (without `positioned`) |\n| `getEventState()` | Same as `getState()` — for event payloads |\n| `setPositionData(name, data)` | Stores beacon/tooltip `PositionData`. Sets `positioned=true` during `*_before` phases |\n| `getPositionData(name)` | Returns cached position data |\n| `cleanupPositionData()` | Clears both position caches (called on step init) |\n| `setSteps(steps)` | Updates internal steps array and size |\n\n`applyTransitions()` auto-promotes `WAITING → RUNNING` when `size > 0`.\n\n## Event System\n\nSingle `onEvent` prop receives `EventData` and `Controls`:\n\n```typescript\ntype EventHandler = (data: EventData, controls: Controls) => void;\n\ntype EventData = TourData & {\n  error: Error | null;      // Only for ERROR events\n  scroll: ScrollData | null; // Only for SCROLL_START/SCROLL_END\n  scrolling: boolean;\n  type: Events;\n  waiting: boolean;\n}\n```\n\nThe `controls` parameter gives event handlers direct access to tour control methods (next, prev, start, stop, etc.).\n\n```mermaid\nsequenceDiagram\n    participant Tour\n    participant Step\n    participant Scroll\n    participant UI\n\n    Tour->>Step: tour:start\n    Step->>Step: step:before_hook\n    Step->>Step: step:before\n    Step->>Scroll: scroll:start\n    Scroll-->>Step: scroll:end\n    Step->>UI: beacon\n    Note over UI: user clicks\n    UI->>UI: tooltip\n    UI->>Step: step:after\n    Step->>Step: step:after_hook\n    Note over Step: repeat per step\n    Step->>Tour: tour:end\n```\n\nAll events: `tour:start`, `step:before_hook`, `step:before`, `scroll:start`, `scroll:end`, `beacon`, `tooltip`, `step:after`, `step:after_hook`, `tour:end`, `tour:status` (on STOP/RESET), `error:target_not_found`, `error`\n\n## Controls (`src/hooks/useControls.ts`)\n\nExtracted hook providing all tour control methods. Created once via `useMemo`, returned to consumers.\n\n| Method | Effect |\n|--------|--------|\n| `close(origin?)` | RUNNING only. Sets `action:CLOSE, index+1, lifecycle:COMPLETE` |\n| `go(nextIndex)` | RUNNING + uncontrolled only. Sets `action:GO, index=nextIndex, lifecycle:COMPLETE`. FINISHED if index >= size |\n| `info()` | Returns current public state |\n| `next()` | RUNNING only. Sets `action:NEXT, index+1 (clamped to size), lifecycle:COMPLETE` |\n| `open()` | RUNNING only. Sets `lifecycle:TOOLTIP_BEFORE` to force-open tooltip |\n| `prev()` | RUNNING only. Sets `action:PREV, index-1 (clamped to 0), lifecycle:COMPLETE` |\n| `replay(origin?)` | RUNNING + lifecycle:TOOLTIP only. Sets `action:REPLAY, lifecycle:COMPLETE`. Index unchanged. Triggers full step re-entry through `step:after` → `step:before` → BEACON/TOOLTIP |\n| `reset(restart?)` | Uncontrolled only. Sets `index:0, lifecycle:INIT, status:READY (or RUNNING if restart)` |\n| `skip(origin?)` | RUNNING only. Sets `action:SKIP, lifecycle:COMPLETE, status:SKIPPED` |\n| `start(nextIndex?)` | Sets `action:START, lifecycle:INIT, status:RUNNING (or WAITING if no steps)`. Uses `forceIndex=true` |\n| `stop(advance?)` | Not if FINISHED/SKIPPED. Sets `action:STOP, lifecycle:COMPLETE, status:PAUSED`. `index+1` if advance |\n\nAll methods also reset `positioned:false, scrolling:false, waiting:false`.\n\n## Orchestration Hooks\n\n### useJoyride (`src/hooks/useJoyride.tsx`)\n\nPublic hook. Calls `useTourEngine`, wraps state (strips `positioned`), creates `TourRenderer` element, returns `{ controls, failures, on, state, step, Tour }`.\n\n### useTourEngine (`src/hooks/useTourEngine.ts`)\n\nCore orchestrator. Creates store once via `useRef(createStore(mergedProps))`, subscribes via `useSyncExternalStore`. Merges props with defaults via `useMemoDeepCompare`.\n\nOn mount: if `run=true` and steps exist and valid, calls `controls.start()`.\n\nDelegates to five sub-hooks:\n- `useControls` — tour control methods\n- `useEventEmitter` — centralized event emission\n- `usePropSync` — external prop change handling\n- `useLifecycleEffect` — state machine progression\n- `useScrollEffect` — scroll-to-target\n\n### useEventEmitter (`src/hooks/useEventEmitter.ts`)\n\nCentralizes all `onEvent` calls into a single `emitEvent(type, step, overrides?)` function. Uses refs for `onEvent` and `controls` to keep callback identity stable. Composes event data from `store.getEventState()` + defaults (`error: null`, `scroll: null`) + step + type + overrides. Passes `controls` as second argument to `onEvent`.\n\nUsed by `usePropSync`, `useLifecycleEffect`, and `useScrollEffect` instead of calling `onEvent` directly.\n\n### usePropSync (`src/hooks/usePropSync.ts`)\n\nHandles external prop changes:\n\n- **Steps change**: Validates steps, calls `store.setSteps(steps)`. Fires `error` event if invalid.\n- **Run toggle**: `run=true` → `controls.start()`, `run=false` → `controls.stop()`\n\n### useLifecycleEffect (`src/hooks/useLifecycleEffect.ts`)\n\nFive separate effects drive the state machine:\n\n**Effect 1: Action tracking** (`deps: [action]`)\nTracks NEXT/PREV/SKIP/CLOSE actions in `lastAction` ref for enriching callbacks.\n\n**Effect 2: Target resolution** (`deps: [index, lifecycle, status, store]`)\nWhen RUNNING + step exists + lifecycle is INIT:\n1. Cleans up position data\n2. If step has `before` hook: sets `waiting=true`, fires `STEP_BEFORE_HOOK`, runs the async hook with AbortController + timeout. On resolve/reject/timeout → sets `lifecycle:READY, waiting:false`\n3. If no `before` hook: checks target visibility\n   - Target visible → `lifecycle:READY`\n   - `targetWaitTimeout=0` → `lifecycle:READY` (proceed without target)\n   - Otherwise → polls at 100ms intervals, sets `waiting=true`. On timeout → `lifecycle:READY`\n\n**Effect 3: Step presentation** (`deps: [lifecycle, store]`)\nWhen lifecycle reaches READY:\n- Target visible + from INIT → fires `STEP_BEFORE` event\n- Target visible → determines final lifecycle (BEACON or TOOLTIP via `hideBeacon()`), checks if scroll needed, sets `lifecycle:*_BEFORE, scrolling:willScroll`\n- Target not visible (RUNNING, not INIT/COMPLETE) → fires `TARGET_NOT_FOUND`, auto-advances in uncontrolled mode\n\n**Effect 4: Transitions + callbacks** (`deps: [lifecycle, positioned, scrolling, store]`)\n- `*_BEFORE` phase + `scrolling=false` → transitions to final BEACON/TOOLTIP\n- BEACON lifecycle → fires `beacon` event\n- TOOLTIP lifecycle → fires `tooltip` event\n- COMPLETE lifecycle (from TOOLTIP) → fires `step:after` event, runs `after` hook (fire-and-forget), fires `step:after_hook` event\n\n**Effect 5: Tour flow** (`deps: [action, index, lifecycle, size, status, store]`)\n- Action REPLAY + COMPLETE → writes `lifecycle:INIT` (works in both controlled and uncontrolled modes; bypasses the controlled-mode pause and the FINISHED branch on the last step to allow same-index re-entry)\n- No step + lifecycle INIT → sets FINISHED\n- Uncontrolled + COMPLETE + index < size → auto-advances to INIT (controlled mode pauses here, waiting for parent to update `stepIndex`)\n- COMPLETE + index >= size → sets FINISHED\n- Status → FINISHED/SKIPPED → fires `tour:end`, calls `controls.reset()`, clears lastAction\n- Status → RUNNING (from IDLE/READY/PAUSED) → fires `tour:start`\n- Action → STOP → fires `tour:status`, clears lastAction\n- Action → RESET → fires `tour:status`, clears lastAction\n\n### useScrollEffect (`src/hooks/useScrollEffect.ts`)\n\nScrolls to target when lifecycle reaches `*_before` phases and `positioned=true` and `scrolling=true`:\n\n- Custom scroll parent: first scrolls page to bring parent into view, then scrolls within parent\n- `adjustForPlacement()`: accounts for placement (top, left, right) to avoid tooltip being off-screen after scroll\n- Fires `scroll:start` and `scroll:end` events with `ScrollData` (initial, target, element, duration)\n- Sets `scrolling:false` on completion, triggering Effect 4's `*_BEFORE → BEACON/TOOLTIP` transition\n- Cancels previous scroll if new one starts\n\n## Step Processing\n\n### Step Merging (`src/modules/step.ts`)\n\n`getMergedStep(props, currentStep)` merges in two passes:\n\n**Pass 1 — step merge:**\n```\ndefaultStep → pick(props, basePropsFields) → currentStep\n```\n\n**Pass 2 — options merge:**\n```\ndefaultOptions → props.options → pick(currentStep, optionFieldNames)\n```\n\nFinal overrides applied after merge:\n- `locale`: deep-merged `defaultLocale → props.locale → step.locale`\n- `floatingOptions`: deep-merged `defaultFloatingOptions → props.floatingOptions → step.floatingOptions`\n- `spotlightPadding`: normalized to `{ top, right, bottom, left }` via `normalizeSpotlightPadding()`\n- `styles`: generated via `getStyles(step)`\n\nSharedProps fields picked from props: `arrowComponent, beaconComponent, floatingOptions, loaderComponent, locale, styles, tooltipComponent`\n\n### Step Hooks\n\nTwo hooks per step, defined as Options (globally or per-step):\n\n**`before(data: TourData) → Promise<void>`**: Blocks the tour at INIT phase. Sets `waiting=true` immediately; shows loader after `loaderDelay` (default 300ms). Capped by `beforeTimeout` (default 5000ms). On error/timeout, fires `error` event and proceeds.\n\n**`after(data: TourData) → void`**: Fire-and-forget after STEP_AFTER. Wrapped in try-catch — user code errors don't break the tour.\n\n### Target Resolution & Tracking\n\n**Resolution** (`getElement()` in `src/modules/dom.ts`):\n- CSS selector string → `document.querySelector()`\n- HTMLElement → used directly\n- React ref → `.current`\n- Function → called, returns element or null\n\n**Visibility** (`isElementVisible()`): walks DOM tree checking `display`/`visibility`.\n\n**Position tracking** (`src/hooks/useTargetPosition.ts`):\n- ResizeObserver on target element\n- Scroll/resize event listeners (passive)\n- Returns `{ top, left, width, height, isFixed }` with spotlightPadding applied\n- Disabled during scroll/waiting to avoid jitter\n\n## Rendering Pipeline\n\n```mermaid\nflowchart TD\n    J[\"Joyride (SSR guard)\"] --> UJ[useJoyride]\n    UJ --> TR[\"TourRenderer (fragment)\"]\n    TR --> S[\"Step\"]\n    TR --> P[\"Portal element\"]\n    S --> FT[useFocusTrap]\n    S --> F[Floater]\n    F --> P\n    P --> B[\"Beacon (BEACON phases)\"]\n    P --> T[\"Tooltip (TOOLTIP phases)\"]\n    P --> A[Arrow]\n    P --> L[\"Loader (waiting=true)\"]\n    P --> O[\"Overlay (SVG spotlight)\"]\n\n    style P fill:#f9f,stroke:#333\n```\n\nAll visible UI renders through Portal to the same portal element — `TourRenderer` and `Step` produce only React fragments, not DOM elements. Floater wraps beacon/tooltip/arrow in `<Portal>`, and TourRenderer wraps overlay/loader in `<Portal>`. Both target the same element from `usePortalElement`.\n\n### Key Components\n\n**Floater** (`src/components/Floater.tsx`):\n- Wraps all rendered content (beacon or tooltip + arrow) in `<Portal>` — nothing renders inline\n- Two `useFloating()` instances: tooltip (full middleware) and beacon (offset only)\n- Tooltip middleware: `offset` (includes spotlightPadding + arrow size), `flip`/`autoPlacement`, `shift`, `arrow`, plus custom middleware from `floatingOptions`\n- Center placement: custom middleware positions at viewport center, uses virtual reference element\n- Strategy: `fixed` when `isFixed=true` or target has fixed/sticky positioning, `absolute` otherwise\n- Reports position data to store via `setPositionData()` on each calculation\n\n**Overlay** (`src/components/Overlay.tsx`):\n- SVG-based with `fillRule=\"evenodd\"` for spotlight cutout\n- `useTargetPosition` tracks target rect via ResizeObserver + scroll/resize listeners\n- Two SVG paths: overlay path (full coverage with cutout) + cover path (fades out to reveal spotlight)\n- Visible during TOOLTIP/TOOLTIP_BEFORE (continuous) or TOOLTIP only (non-continuous); also during waiting\n- Hidden during BEACON/BEACON_BEFORE phases\n\n**Beacon** (`src/components/Beacon.tsx`):\n- Pulsing animation via CSS keyframes (injected once via style tag with existence check)\n- Default: button with inner/outer animated spans\n- Custom: `beaconComponent` prop receives render props\n- Auto-focuses on mount if `shouldFocus=true`\n\n**Tooltip** (`src/components/Tooltip/index.tsx`):\n- Generates button props (back, close, primary, skip) with handlers\n- Primary button: \"Next\" (continuous + not last), \"Last\" (last step), \"Close\" (non-continuous)\n- Delegates to DefaultTooltip (default) or `tooltipComponent` (custom)\n\n**DefaultTooltip** (`src/components/Tooltip/DefaultTooltip.tsx`):\n- Default layout: title → content → footer (skip, back, primary buttons) → close button\n- Footer hidden if `buttons` array has no footer buttons (back/primary/skip)\n\n**Arrow** (`src/components/Arrow.tsx`):\n- Default: SVG arrow element positioned by Floating UI middleware\n- Custom: `arrowComponent` prop receives `ArrowRenderProps`\n\n**Loader** (`src/components/Loader.tsx`):\n- Shown when `waiting=true` (target polling or `before` hook)\n- Custom: `loaderComponent` prop. Set to `null` to disable\n\n**Portal** (`src/components/Portal.tsx`):\n- `createPortal(children, element)` wrapper\n\n**usePortalElement** (`src/hooks/usePortalElement.ts`):\n- Creates `<div id=\"react-joyride-portal\">` in body, or uses provided `portalElement` (selector or HTMLElement)\n- Cleans up auto-created div on unmount\n\n## Appendix\n\n### DOM Utilities (`src/modules/dom.ts`)\n\n- `hasPosition(el, type?)`: Recursively checks for fixed/sticky positioning\n- `getScrollParent(element)`: Uses `scrollparent` library\n- `hasCustomScrollParent(element)`: Checks if scroll parent differs from document\n- `getScrollTo(element, offset)`: Calculates scroll position\n- `getScrollTargetToCenter(element)`: Centers element in viewport\n- `scrollTo(value, options)`: Smooth scroll via `scroll` library; returns `{ cancel, promise }`\n- `scrollDocument()`: Returns document scrolling element\n- `getDocumentHeight(median?)`: Takes max or median of 5 height measurements\n- `canUseDOM()`: SSR safety check\n\n### Focus Trap (`src/hooks/useFocusTrap.ts`)\n\n- Intercepts Tab key within tooltip element\n- Wraps focus forward/backward at tabbable element boundaries\n- Focuses initial selector (`[data-action=primary]`) on mount\n- Disabled when `step.disableFocusTrap=true`\n- Restores previous focus on cleanup\n\n### Styles (`src/styles.ts`)\n\n`getStyles(step)`:\n1. Merges step styles with generated defaults\n2. Adaptive width: `min(step.width, window.innerWidth - 30)`\n3. Generates style objects (beacon, buttons, tooltip, overlay, arrow, etc.)\n4. Returns merged with user overrides\n\nSee `src/defaults.ts` for current default values.\n\n### Types (`src/types/`)\n\n| File | Description |\n|------|-------------|\n| `common.ts` | Actions, Events, Lifecycle, Origin, Status, Placement, Locale, Options, Styles |\n| `components.ts` | ArrowRenderProps, BeaconRenderProps, LoaderRenderProps, TooltipRenderProps |\n| `events.ts` | EventData, EventHandler `(data, controls)`, TourData, ScrollData |\n| `floating.ts` | FloatingOptions, PositionData |\n| `props.ts` | Props, SharedProps, UseJoyrideReturn, StepFailure |\n| `state.ts` | Controls, State |\n| `step.ts` | Step, StepMerged, StepTarget, SelectorOrElement |\n| `utilities.ts` | PartialDeep, Simplify, SetRequired, ValueOf |\n\n### Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `@floating-ui/react-dom` | Tooltip/beacon positioning (dual instances) |\n| `@fastify/deepmerge` | Deep object merging (React element aware) |\n| `@gilbarbara/hooks` | useMemoDeepCompare, useMount, usePrevious, useUpdateEffect, useWindowSize |\n| `@gilbarbara/deep-equal` | Deep equality checking (store change detection) |\n| `@gilbarbara/types` | Utility types |\n| `scroll` / `scrollparent` | Smooth scrolling and scroll parent detection |\n| `react-innertext` | Extract text from React elements (button labels) |\n| `is-lite` | Type checking utilities |\n\n<!-- File: skills/react-joyride/references/api-events-components.md (Tokens: ~1210 | Category: anthropic-skill) -->\n# Events, Constants, Custom Components\n\n## Constants\n\n### ACTIONS\n```typescript\nconst ACTIONS = {\n  INIT: 'init',\n  START: 'start',\n  STOP: 'stop',\n  RESET: 'reset',\n  PREV: 'prev',\n  NEXT: 'next',\n  GO: 'go',\n  CLOSE: 'close',\n  SKIP: 'skip',\n  UPDATE: 'update',\n  COMPLETE: 'complete',\n} as const;\n```\n\n### EVENTS\n```typescript\nconst EVENTS = {\n  TOUR_START: 'tour:start',\n  STEP_BEFORE_HOOK: 'step:before_hook',\n  STEP_BEFORE: 'step:before',\n  SCROLL_START: 'scroll:start',\n  SCROLL_END: 'scroll:end',\n  BEACON: 'beacon',\n  TOOLTIP: 'tooltip',\n  STEP_AFTER: 'step:after',\n  STEP_AFTER_HOOK: 'step:after_hook',\n  TOUR_END: 'tour:end',\n  TOUR_STATUS: 'tour:status',\n  TARGET_NOT_FOUND: 'error:target_not_found',\n  ERROR: 'error',\n} as const;\n```\n\n### LIFECYCLE\n```typescript\nconst LIFECYCLE = {\n  INIT: 'init',\n  READY: 'ready',\n  BEACON_BEFORE: 'beacon_before',\n  BEACON: 'beacon',\n  TOOLTIP_BEFORE: 'tooltip_before',\n  TOOLTIP: 'tooltip',\n  COMPLETE: 'complete',\n} as const;\n```\n\n### STATUS\n```typescript\nconst STATUS = {\n  IDLE: 'idle',\n  READY: 'ready',\n  WAITING: 'waiting',\n  RUNNING: 'running',\n  PAUSED: 'paused',\n  SKIPPED: 'skipped',\n  FINISHED: 'finished',\n} as const;\n```\n\n### ORIGIN\n```typescript\nconst ORIGIN = {\n  BUTTON_CLOSE: 'button_close',\n  BUTTON_SKIP: 'button_skip',\n  BUTTON_PRIMARY: 'button_primary',\n  KEYBOARD: 'keyboard',\n  OVERLAY: 'overlay',\n} as const;\n```\n\n## Event Types\n\n### EventData\n\nThe full payload passed to `onEvent` and event subscribers:\n\n```typescript\ntype EventData = TourData & {\n  error: Error | null;         // Only for 'error' events\n  scroll: ScrollData | null;   // Only for 'scroll:start' / 'scroll:end'\n  scrolling: boolean;\n  type: Events;                // The event type (discriminator)\n  waiting: boolean;\n}\n```\n\n### TourData\n\nBase payload (also passed to `before` and `after` hooks):\n\n```typescript\ninterface TourData {\n  action: Actions;\n  controlled: boolean;\n  index: number;\n  lifecycle: Lifecycle;\n  origin: Origin | null;\n  size: number;\n  status: Status;\n  step: StepMerged;\n}\n```\n\n### EventHandler\n\n```typescript\ntype EventHandler = (data: EventData, controls: Controls) => void;\n```\n\n### ScrollData\n\n```typescript\ntype ScrollData = {\n  duration: number;     // Scroll duration in ms\n  element: Element;     // Element being scrolled\n  initial: number;      // Scroll position before\n  target: number;       // Computed scroll destination\n}\n```\n\n## Event Flow (per step)\n\n```\ntour:start (once)\n  |\n  v\nstep:before_hook  (if before hook exists)\n  |\n  v\nstep:before  (target found, step about to render)\n  |\n  v\nscroll:start / scroll:end  (if scrolling needed)\n  |\n  v\nbeacon  (if beacon shown) OR tooltip  (if beacon skipped)\n  |\n  v  (user clicks beacon)\ntooltip\n  |\n  v  (user clicks next/prev/close/skip)\nstep:after\n  |\n  v\nstep:after_hook  (if after hook exists)\n  |\n  v  (next step or tour end)\ntour:end  (when finished/skipped)\n```\n\n## Custom Component Render Props\n\n### TooltipRenderProps\n\n```typescript\ntype TooltipRenderProps = {\n  // Tour state\n  continuous: boolean;\n  index: number;\n  isLastStep: boolean;\n  size: number;\n  step: StepMerged;\n\n  // Button props (spread on button elements)\n  backProps: {\n    'aria-label': string;\n    'data-action': string;\n    onClick: MouseEventHandler<HTMLElement>;\n    role: string;\n    title: string;\n  };\n  closeProps: { /* same shape */ };\n  primaryProps: { /* same shape */ };\n  skipProps: { /* same shape */ };\n\n  // Container props (spread on tooltip wrapper)\n  tooltipProps: {\n    'aria-modal': boolean;\n    role: string;  // 'dialog'\n  };\n\n  // Tour controls\n  controls: Controls;\n}\n```\n\n**Important**: Always spread `tooltipProps` on the root element and button props on their respective buttons. This ensures correct accessibility attributes and action handling.\n\n### BeaconRenderProps\n\n```typescript\ntype BeaconRenderProps = {\n  continuous: boolean;\n  index: number;\n  isLastStep: boolean;\n  size: number;\n  step: StepMerged;\n}\n```\n\nMust render a `<span>` since it's placed inside a `<button>` wrapper.\n\n### ArrowRenderProps\n\n```typescript\ntype ArrowRenderProps = {\n  base: number;          // Arrow base width\n  placement: Placement;  // Computed placement\n  size: number;          // Arrow height\n}\n```\n\n### LoaderRenderProps\n\n```typescript\ntype LoaderRenderProps = {\n  step: StepMerged;\n}\n```\n\nSet `loaderComponent={null}` on Props or Step to disable the loader entirely.\n\nDocs: https://v3.react-joyride.com/docs/custom-components\n\n<!-- File: skills/react-joyride/references/api-props-options.md (Tokens: ~1708 | Category: anthropic-skill) -->\n# Props, Options, Locale, FloatingOptions, Styles\n\n## Props\n\n```typescript\ntype Props = SharedProps & {\n  continuous?: boolean;        // Sequential play with Next button (default: false)\n  debug?: boolean;             // Log to console (default: false)\n  initialStepIndex?: number;   // Start index for uncontrolled mode (default: 0)\n  nonce?: string;              // CSP nonce for inline styles\n  onEvent?: EventHandler;      // (data: EventData, controls: Controls) => void\n  options?: Partial<Options>;  // Default options for all steps\n  portalElement?: string | HTMLElement; // Render tooltip into this element\n  run?: boolean;               // Start/stop tour (default: false)\n  scrollToFirstStep?: boolean; // Scroll to first step on start (default: false)\n  stepIndex?: number;          // Controlled mode: manages step index externally\n  steps: Array<Step>;          // Required: tour steps\n}\n```\n\n## SharedProps\n\nInherited by both `Props` and `Step`:\n\n```typescript\ntype SharedProps = {\n  arrowComponent?: ElementType<ArrowRenderProps>;\n  beaconComponent?: ElementType<BeaconRenderProps>;\n  floatingOptions?: Partial<FloatingOptions>;\n  loaderComponent?: ElementType<LoaderRenderProps> | null; // null disables loader\n  locale?: Locale;\n  styles?: PartialDeep<Styles>;\n  tooltipComponent?: ElementType<TooltipRenderProps>;\n}\n```\n\n## Options\n\nAll fields can be set globally via `options` prop or per-step (step overrides global).\n\n### Appearance\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `backgroundColor` | `string` | `'#ffffff'` | Tooltip background |\n| `primaryColor` | `string` | `'#000000'` | Primary button and beacon |\n| `textColor` | `string` | `'#000000'` | Tooltip text |\n| `overlayColor` | `string` | `'#00000080'` | Overlay backdrop |\n| `arrowColor` | `string` | `'#ffffff'` | Arrow fill |\n| `width` | `string \\| number` | `380` | Tooltip width |\n| `zIndex` | `number` | `100` | z-index for overlay/tooltip |\n\n### Arrow\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `arrowBase` | `number` | `32` | Base width in pixels |\n| `arrowSize` | `number` | `16` | Height/depth in pixels |\n| `arrowSpacing` | `number` | `12` | Distance from tooltip edge |\n\n### Beacon\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `beaconSize` | `number` | `36` | Diameter in pixels |\n| `beaconTrigger` | `'click' \\| 'hover'` | `'click'` | How to open tooltip from beacon |\n\n### Overlay & Spotlight\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `hideOverlay` | `boolean` | `false` | Don't show overlay |\n| `blockTargetInteraction` | `boolean` | `false` | Block pointer events on target |\n| `spotlightRadius` | `number` | `4` | Border radius of cutout |\n| `spotlightPadding` | `number \\| SpotlightPadding` | `10` | Padding around target |\n\n```typescript\ninterface SpotlightPadding { top?: number; right?: number; bottom?: number; left?: number; }\n```\n\n### Buttons & Interactions\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `buttons` | `ButtonType[]` | `['back','close','primary']` | Buttons in tooltip |\n| `closeButtonAction` | `'close' \\| 'skip'` | `'close'` | Close button behavior |\n| `overlayClickAction` | `'close' \\| 'next' \\| false` | `'close'` | Overlay click behavior |\n| `dismissKeyAction` | `'close' \\| 'next' \\| false` | `'close'` | ESC key behavior |\n| `showProgress` | `boolean` | `false` | Show \"N of Total\" in tooltip |\n\n`ButtonType = 'back' | 'close' | 'primary' | 'skip'`\n\n### Scroll\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `skipBeacon` | `boolean` | `false` | Skip beacon, show tooltip directly |\n| `skipScroll` | `boolean` | `false` | Don't scroll to target |\n| `scrollDuration` | `number` | `300` | Scroll animation ms |\n| `scrollOffset` | `number` | `20` | Scroll distance from element |\n| `offset` | `number` | `10` | Distance between tooltip and spotlight |\n\n### Timing & Async\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `before` | `BeforeHook` | - | `(data: TourData) => Promise<void>` — runs before step |\n| `after` | `AfterHook` | - | `(data: TourData) => void` — runs after step (fire-and-forget) |\n| `beforeTimeout` | `number` | `5000` | Max wait for before hook (0 = no timeout) |\n| `targetWaitTimeout` | `number` | `1000` | Max wait for target to appear (0 = no waiting) |\n| `loaderDelay` | `number` | `300` | Delay before showing loader |\n| `disableFocusTrap` | `boolean` | `false` | Disable focus trap on tooltip |\n\n## Locale\n\n```typescript\ninterface Locale {\n  back?: ReactNode;              // 'Back'\n  close?: ReactNode;             // 'Close'\n  last?: ReactNode;              // 'Last'\n  next?: ReactNode;              // 'Next'\n  nextWithProgress?: ReactNode;  // 'Next ({current} of {total})'\n  open?: ReactNode;              // 'Open the dialog'\n  skip?: ReactNode;              // 'Skip'\n}\n```\n\n`{current}` and `{total}` are placeholder strings replaced at render time in `nextWithProgress`.\n\n## FloatingOptions\n\nControls `@floating-ui/react-dom` positioning:\n\n```typescript\ninterface FloatingOptions {\n  autoUpdate?: Partial<AutoUpdateOptions>;     // Auto-reposition options\n  beaconOptions?: { offset?: number };         // Beacon offset (default: -18)\n  flipOptions?: Partial<FlipOptions> | false;  // Flip middleware (false to disable)\n  hideArrow?: boolean;                         // Hide arrow (default: false)\n  middleware?: Array<Middleware>;               // Additional Floating UI middleware\n  onPosition?: (data: PositionData) => void;   // Position callback\n  shiftOptions?: Partial<ShiftOptions>;        // Shift middleware options\n  strategy?: 'fixed' | 'absolute';             // Auto-detected from step.isFixed\n}\n```\n\nDocs: https://v3.react-joyride.com/docs/props/floating-options\n\n## Styles\n\nOverride CSS for any UI element:\n\n```typescript\ninterface Styles {\n  arrow: CSSProperties;\n  beacon: CSSProperties;\n  beaconInner: CSSProperties;\n  beaconOuter: CSSProperties;\n  beaconWrapper: CSSProperties;\n  buttonBack: CSSProperties;\n  buttonClose: CSSProperties;\n  buttonPrimary: CSSProperties;\n  buttonSkip: CSSProperties;\n  floater: CSSProperties;\n  loader: CSSProperties;\n  overlay: CSSProperties;\n  tooltip: CSSProperties;\n  tooltipContainer: CSSProperties;\n  tooltipContent: CSSProperties;\n  tooltipFooter: CSSProperties;\n  tooltipFooterSpacer: CSSProperties;\n  tooltipTitle: CSSProperties;\n}\n```\n\nUse `PartialDeep<Styles>` — only override what you need.\n\nDocs: https://v3.react-joyride.com/docs/props/styles\n\n<!-- File: skills/react-joyride/references/api-step-state-controls.md (Tokens: ~1070 | Category: anthropic-skill) -->\n# Step, State, Controls\n\n## Step\n\nA single step provided by the user. Extends `SharedProps` and `Partial<Options>`.\n\n```typescript\ntype Step = SharedProps & Partial<Options> & {\n  // Required\n  content: ReactNode;                           // Tooltip body\n  target: StepTarget;                           // Element to highlight\n\n  // Optional\n  id?: string;                                  // Unique identifier\n  title?: ReactNode;                            // Tooltip title\n  data?: any;                                   // Custom data (passed to callbacks)\n\n  // Positioning\n  placement?: Placement | 'auto' | 'center';   // Default: 'bottom'\n  beaconPlacement?: Placement;                  // Beacon-specific placement\n  isFixed?: boolean;                            // Force fixed positioning (default: false)\n\n  // Alternate targets\n  scrollTarget?: StepTarget;                    // Scroll to this instead of target\n  spotlightTarget?: StepTarget;                 // Highlight this instead of target\n}\n```\n\nAll `Options` fields (see api-props-options.md) are also valid on Step for per-step overrides.\n\n## StepTarget\n\nFour ways to specify a target element:\n\n```typescript\ntype StepTarget =\n  | string                              // CSS selector\n  | HTMLElement                         // Direct element reference\n  | RefObject<HTMLElement | null>       // React ref\n  | (() => HTMLElement | null);         // Function (re-evaluated each lifecycle)\n```\n\n## StepMerged\n\nThe normalized step after defaults are applied. All `Options` fields become required, plus:\n- `spotlightPadding: Required<SpotlightPadding>` (always `{ top, right, bottom, left }`)\n- `styles: Styles` (fully resolved)\n\nThis is what you receive in event callbacks and custom component props.\n\n## State\n\n```typescript\ntype State = {\n  action: Actions;         // What triggered the update (init, start, next, prev, etc.)\n  controlled: boolean;     // true when stepIndex prop is set\n  index: number;           // Current step index\n  lifecycle: Lifecycle;     // Step rendering phase\n  origin: Origin | null;   // UI element that triggered action\n  scrolling: boolean;      // Scroll animation in progress\n  size: number;            // Total number of steps\n  status: Status;          // Tour status (idle, ready, running, etc.)\n  waiting: boolean;        // Blocked on before hook or target polling\n}\n```\n\n## Controls\n\nAll 10 methods for programmatic tour control:\n\n```typescript\ntype Controls = {\n  close(origin?: Origin | null): void;\n  // Close the current step and advance to the next one.\n  // Optional origin for event tracking.\n\n  go(nextIndex: number): void;\n  // Jump to a specific step by index.\n  // Uncontrolled mode only — logs warning in controlled mode.\n\n  info(): State;\n  // Get the current tour state snapshot.\n\n  next(): void;\n  // Advance to the next step.\n\n  open(): void;\n  // Open the tooltip for the current step (skip beacon).\n\n  prev(): void;\n  // Go back to the previous step.\n\n  reset(restart?: boolean): void;\n  // Reset the tour to the beginning.\n  // If restart=true, also starts the tour.\n  // Uncontrolled mode only — logs warning in controlled mode.\n\n  skip(origin?: 'button_close' | 'button_skip'): void;\n  // Skip (end) the tour entirely. Sets status to SKIPPED.\n\n  start(nextIndex?: number): void;\n  // Start the tour. Optionally at a specific step index.\n\n  stop(advance?: boolean): void;\n  // Stop (pause) the tour. Sets status to PAUSED.\n  // If advance=true, moves to next step before stopping.\n}\n```\n\n## UseJoyrideReturn\n\n```typescript\ntype UseJoyrideReturn = {\n  controls: Controls;\n  // Programmatic tour control methods.\n\n  failures: StepFailure[];\n  // Steps that failed (target not found, before hook errors).\n  // Clears on start/reset.\n\n  on: (eventType: Events, handler: EventHandler) => () => void;\n  // Subscribe to a specific event type.\n  // Returns an unsubscribe function.\n\n  state: State;\n  // Current tour state (reactive, updates on changes).\n\n  step: StepMerged | null;\n  // Current merged step, or null if no step is active.\n\n  Tour: ReactElement | null;\n  // The tour React element. Render this in your JSX.\n}\n```\n\n## StepFailure\n\n```typescript\ninterface StepFailure {\n  reason: 'before_hook' | 'target_not_found';\n  step: StepMerged;\n}\n```\n\n## Placement\n\n```typescript\ntype Placement =\n  | 'top' | 'top-start' | 'top-end'\n  | 'bottom' | 'bottom-start' | 'bottom-end'\n  | 'left' | 'left-start' | 'left-end'\n  | 'right' | 'right-start' | 'right-end';\n\n// Step also accepts: 'auto' (Floating UI auto-placement) and 'center' (modal-style)\n```\n\n<!-- File: skills/react-joyride/references/patterns.md (Tokens: ~1790 | Category: anthropic-skill) -->\n# Common Patterns\n\n## Async Transitions with `before`/`after` Hooks (Preferred)\n\nThe library handles waiting for async operations. Use `before` hooks to prepare the UI before a step shows (open dropdowns, load data, trigger animations). The tour waits for the promise to resolve. Use `after` hooks for cleanup (fire-and-forget, does not block).\n\nThis is the **preferred approach** over controlled mode for most async scenarios.\n\n```tsx\nimport { useState } from 'react';\nimport { ACTIONS, STATUS, useJoyride } from 'react-joyride';\n\nfunction Dashboard() {\n  const [run, setRun] = useState(false);\n  const [isMenuOpen, setIsMenuOpen] = useState(false);\n\n  const { Tour } = useJoyride({\n    continuous: true,\n    run,\n    steps: [\n      {\n        target: '.nav-bar',\n        content: 'This is the navigation',\n        skipBeacon: true,\n      },\n      {\n        target: '.dropdown-item', // Element inside the dropdown\n        content: 'This dropdown item is important',\n        skipBeacon: true,\n        before: ({ action }) => {\n          // Open dropdown before this step — runs on both forward and back navigation\n          setIsMenuOpen(true);\n          return new Promise(resolve => setTimeout(resolve, 300));\n        },\n      },\n      {\n        target: '.main-content',\n        content: 'And here is the main content',\n        before: ({ action }) => {\n          // Close dropdown when moving to this step\n          setIsMenuOpen(false);\n          return new Promise(resolve => setTimeout(resolve, 300));\n        },\n      },\n    ],\n    onEvent: (data) => {\n      if ([STATUS.FINISHED, STATUS.SKIPPED].includes(data.status)) {\n        setRun(false);\n        setIsMenuOpen(false);\n      }\n    },\n  });\n\n  return (\n    <div>\n      {Tour}\n      <button onClick={() => setRun(true)}>Start Tour</button>\n      {/* ... */}\n    </div>\n  );\n}\n```\n\n## Before/After Hooks\n\n### Async data loading before a step\n\n```tsx\n{\n  target: '.user-profile',\n  content: 'Here is your profile data',\n  before: async () => {\n    await fetchUserProfile();\n    // Tour waits for this promise to resolve\n  },\n  beforeTimeout: 10000, // Allow up to 10s\n}\n```\n\n### Analytics tracking after a step\n\n```tsx\n{\n  target: '.feature',\n  content: 'Check out this feature',\n  after: (data) => {\n    // Fire-and-forget — does not block the tour\n    analytics.track('tour_step_completed', {\n      stepIndex: data.index,\n      action: data.action,\n    });\n  },\n}\n```\n\n### Delayed transitions (e.g., waiting for animation)\n\n```tsx\n{\n  target: '.sidebar',\n  content: 'The sidebar',\n  before: ({ action }) => {\n    // Only delay when navigating forward (not on back)\n    const ms = action === ACTIONS.PREV ? 0 : 300;\n    return new Promise(resolve => setTimeout(resolve, ms));\n  },\n}\n```\n\n## Custom Tooltip Component\n\n```tsx\nimport type { TooltipRenderProps } from 'react-joyride';\n\nfunction CustomTooltip({\n  backProps,\n  closeProps,\n  index,\n  isLastStep,\n  primaryProps,\n  size,\n  skipProps,\n  step,\n  tooltipProps,\n}: TooltipRenderProps) {\n  return (\n    <div\n      {...tooltipProps}\n      style={{\n        background: '#fff',\n        borderRadius: 8,\n        maxWidth: 400,\n        padding: 20,\n        width: step.width,\n      }}\n    >\n      {step.title && <h3 style={{ margin: '0 0 8px' }}>{step.title}</h3>}\n      <div>{step.content}</div>\n\n      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 16 }}>\n        {step.buttons.includes('skip') && !isLastStep && (\n          <button {...skipProps} type=\"button\">Skip</button>\n        )}\n        <div style={{ display: 'flex', gap: 8, marginLeft: 'auto' }}>\n          {index > 0 && (\n            <button {...backProps} type=\"button\">Back</button>\n          )}\n          <button {...primaryProps} type=\"button\">\n            {isLastStep ? 'Done' : `Next (${index + 1}/${size})`}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n// Usage:\n// <Joyride tooltipComponent={CustomTooltip} ... />\n// or in useJoyride: useJoyride({ tooltipComponent: CustomTooltip, ... })\n```\n\n## Event Subscription with `on()`\n\n```tsx\nfunction App() {\n  const { on, Tour } = useJoyride({\n    steps,\n    run: true,\n    continuous: true,\n  });\n\n  useEffect(() => {\n    const unsubs = [\n      on('tour:start', (data) => {\n        console.log('Tour started');\n      }),\n      on('tooltip', (data) => {\n        analytics.track('step_viewed', { index: data.index });\n      }),\n      on('tour:end', (data) => {\n        const wasSkipped = data.status === 'skipped';\n        analytics.track('tour_ended', { skipped: wasSkipped, lastStep: data.index });\n      }),\n    ];\n\n    return () => unsubs.forEach(unsub => unsub());\n  }, [on]);\n\n  return <div>{Tour}</div>;\n}\n```\n\n## Dynamic Steps\n\n```tsx\nfunction App() {\n  const [steps, setSteps] = useState<Step[]>([]);\n\n  useEffect(() => {\n    // Build steps based on feature flags, user role, etc.\n    const dynamicSteps: Step[] = [\n      { target: '.dashboard', content: 'Welcome to your dashboard' },\n    ];\n\n    if (user.isAdmin) {\n      dynamicSteps.push({\n        target: '.admin-panel',\n        content: 'Admin controls are here',\n      });\n    }\n\n    if (featureFlags.newSearch) {\n      dynamicSteps.push({\n        target: '.search-bar',\n        content: 'Try the new search',\n      });\n    }\n\n    setSteps(dynamicSteps);\n  }, [user, featureFlags]);\n\n  return (\n    <Joyride\n      run={steps.length > 0}\n      steps={steps}\n      continuous\n    />\n  );\n}\n```\n\n## React Ref Targets\n\n```tsx\nfunction App() {\n  const sidebarRef = useRef<HTMLDivElement>(null);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n\n  const steps: Step[] = [\n    { target: sidebarRef, content: 'Navigation sidebar' },\n    { target: buttonRef, content: 'Click here to create' },\n    { target: '.css-selector', content: 'Mix refs with selectors' },\n  ];\n\n  return (\n    <div>\n      <Joyride steps={steps} run continuous />\n      <div ref={sidebarRef}>Sidebar</div>\n      <button ref={buttonRef}>Create</button>\n      <span className=\"css-selector\">Other element</span>\n    </div>\n  );\n}\n```\n\n## Restart / Resume Tour\n\n```tsx\nfunction App() {\n  const [run, setRun] = useState(false);\n  const [initialStepIndex, setInitialStepIndex] = useState(0);\n\n  const handleStart = () => {\n    setInitialStepIndex(0);\n    setRun(true);\n  };\n\n  const handleResume = (fromStep: number) => {\n    setInitialStepIndex(fromStep);\n    setRun(true);\n  };\n\n  return (\n    <div>\n      <Joyride\n        run={run}\n        initialStepIndex={initialStepIndex}\n        steps={steps}\n        continuous\n        onEvent={(data) => {\n          if ([STATUS.FINISHED, STATUS.SKIPPED].includes(data.status)) {\n            setRun(false);\n          }\n        }}\n      />\n      <button onClick={handleStart}>Start Tour</button>\n      <button onClick={() => handleResume(3)}>Resume from Step 4</button>\n    </div>\n  );\n}\n```\n\n## Center Placement (Modal Style)\n\n```tsx\n{\n  target: 'body',\n  placement: 'center',\n  content: (\n    <div>\n      <h2>Welcome!</h2>\n      <p>This appears as a centered modal overlay.</p>\n    </div>\n  ),\n  // Center placement automatically hides beacon and arrow\n}\n```\n\n<!-- File: skills/react-joyride/SKILL.md (Tokens: ~4026 | Category: anthropic-skill) -->\n---\nname: react-joyride\ndescription: >-\n  Guide for implementing, configuring, and debugging React Joyride v3 guided tours.\n  Use this skill whenever the user mentions joyride, guided tour, onboarding tour,\n  walkthrough, tooltip tour, step-by-step guide, product tour, or wants to highlight\n  UI elements sequentially. Also use when debugging tour issues like tooltips not\n  appearing, targets not found, or controlled mode problems. This skill covers the\n  useJoyride hook, Joyride component, step configuration, events, controls, custom\n  components, and styling.\nlicense: MIT\nmetadata:\n  author: gilbarbara\n  version: \"3.0.0\"\n---\n\n# React Joyride v3\n\nCreate guided tours in React apps. Two public APIs: the `useJoyride()` hook (recommended) and the `<Joyride>` component.\n\nOnline docs: https://v3.react-joyride.com\n\n## Quick Start\n\n### Using the hook (recommended)\n\n```tsx\nimport { useJoyride, STATUS, Status } from 'react-joyride';\n\nfunction App() {\n  const { Tour } = useJoyride({\n    continuous: true,\n    run: true,\n    steps: [\n      { target: '.my-element', content: 'This is the first step', title: 'Welcome' },\n      { target: '#sidebar', content: 'Navigate here', placement: 'right' },\n    ],\n    onEvent: (data) => {\n      if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n        // Tour ended\n      }\n    },\n  });\n\n  return <div>{Tour}{/* rest of app */}</div>;\n}\n```\n\n### Using the component\n\n```tsx\nimport { Joyride, STATUS, Status } from 'react-joyride';\n\nfunction App() {\n  return (\n    <Joyride\n      continuous\n      run={true}\n      steps={[\n        { target: '.my-element', content: 'First step' },\n        { target: '#sidebar', content: 'Second step' },\n      ]}\n      onEvent={(data) => {\n        if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n          // Tour ended\n        }\n      }}\n    />\n  );\n}\n```\n\nThe hook returns `{ controls, failures, on, state, step, Tour }`. Render `Tour` in your JSX.\n\nDocs: https://v3.react-joyride.com/docs/getting-started\n\n## Core Concepts\n\nThe tour has two state dimensions:\n\n**Tour Status**: `idle -> ready -> waiting -> running <-> paused -> finished | skipped`\n- `idle`: No steps loaded\n- `ready`: Steps loaded, waiting for `run: true`\n- `waiting`: `run=true` but steps loading async (transitions to running when steps arrive)\n- `running`: Tour active\n- `paused`: Tour paused (controlled mode at COMPLETE, or `stop()` called)\n- `finished` / `skipped`: Tour ended\n\n**Step Lifecycle** (per step): `init -> ready -> beacon_before -> beacon -> tooltip_before -> tooltip -> complete`\n- `*_before` phases: scrolling and positioning happen here\n- `beacon`: Pulsing indicator shown (skipped when `continuous` + navigating, `skipBeacon`, or `placement: 'center'`)\n- `tooltip`: The tooltip is visible and interactive\n\nDocs: https://v3.react-joyride.com/docs/how-it-works\n\n## Step Configuration\n\nEach step requires `target` and `content`. All other fields are optional.\n\n```tsx\n{\n  target: '.my-element',       // CSS selector, HTMLElement, React ref, or () => HTMLElement\n  content: 'Step body text',   // ReactNode\n  title: 'Optional title',    // ReactNode\n  placement: 'bottom',        // Default. Also: top, left, right, *-start, *-end, auto, center\n  id: 'unique-id',            // Optional identifier\n  data: { custom: 'data' },   // Attached to event callbacks\n}\n```\n\n### Target types\n\n```tsx\n// CSS selector\n{ target: '.sidebar-nav' }\n// HTMLElement\n{ target: document.getElementById('my-el') }\n// React ref\nconst ref = useRef(null);\n{ target: ref }\n// Function (evaluated each lifecycle)\n{ target: () => document.querySelector('.dynamic-element') }\n```\n\n### Common step options (override per-step)\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `placement` | `'bottom'` | Tooltip position. Use `'center'` for modal-style (requires `target: 'body'`) |\n| `skipBeacon` | `false` | Skip beacon, show tooltip directly |\n| `buttons` | `['back','close','primary']` | Buttons in tooltip. Add `'skip'` for skip button |\n| `hideOverlay` | `false` | Don't show dark overlay |\n| `blockTargetInteraction` | `false` | Block clicks on highlighted element |\n| `before` | - | `(data) => Promise<void>` — async hook before step shows |\n| `after` | - | `(data) => void` — fire-and-forget hook after step completes |\n| `skipScroll` | `false` | Don't scroll to target |\n| `scrollTarget` | - | Scroll to this element instead of `target` |\n| `spotlightTarget` | - | Highlight this element instead of `target` |\n| `spotlightPadding` | `10` | Padding around spotlight. Number or `{ top, right, bottom, left }` |\n| `targetWaitTimeout` | `1000` | ms to wait for target to appear. `0` = no waiting |\n| `beforeTimeout` | `5000` | ms to wait for `before` hook. `0` = no timeout |\n\nAll `Options` fields can be set globally via `options` prop or per-step. Per-step values override global.\n\nDocs: https://v3.react-joyride.com/docs/step | https://v3.react-joyride.com/docs/props/options\n\n## Uncontrolled vs Controlled\n\n### Uncontrolled (default — strongly preferred)\n\nThe tour manages step navigation internally. This is the right choice for most use cases.\n\n**The library handles async transitions for you.** If a step needs to wait for a UI change (dropdown opening, data loading, animation), use `before` hooks — the tour waits for the promise to resolve before showing the step. If a target element isn't in the DOM yet, `targetWaitTimeout` (default: 1000ms) handles polling for it. You do NOT need controlled mode for these cases.\n\n```tsx\nconst { Tour } = useJoyride({\n  continuous: true,\n  run: isRunning,\n  steps: [\n    { target: '.nav', content: 'Navigation' },\n    {\n      target: '.dropdown-item',\n      content: 'Inside the dropdown',\n      before: () => {\n        // Open dropdown and wait for animation — tour waits automatically\n        openDropdown();\n        return new Promise(resolve => setTimeout(resolve, 300));\n      },\n      after: () => closeDropdown(), // Clean up after step (fire-and-forget)\n    },\n    { target: '.main-content', content: 'Main content' },\n  ],\n  onEvent: (data) => {\n    if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(data.status)) {\n      setIsRunning(false);\n    }\n  },\n});\n```\n\n### Controlled (with `stepIndex`) — use sparingly\n\nOnly use controlled mode when the parent genuinely needs to manage the step index externally (e.g., syncing with URL params, external state machines, or complex multi-component coordination that `before`/`after` hooks can't handle).\n\n```tsx\nconst [stepIndex, setStepIndex] = useState(0);\nconst [run, setRun] = useState(true);\n\nconst { Tour } = useJoyride({\n  continuous: true,\n  run,\n  stepIndex,  // This makes it controlled\n  steps,\n  onEvent: (data) => {\n    const { action, index, status, type } = data;\n\n    if (([STATUS.FINISHED, STATUS.SKIPPED] as Status).includes(status)) {\n      setRun(false);\n      return;\n    }\n\n    if (type === 'step:after' || type === 'error:target_not_found') {\n      setStepIndex(index + (action === 'prev' ? -1 : 1));\n    }\n  },\n});\n```\n\n**Controlled mode rules:**\n- `go()` and `reset()` are disabled (logged warning)\n- You must update `stepIndex` in response to events\n- The tour pauses at COMPLETE — you must advance it\n- Prefer uncontrolled mode with `before`/`after` hooks unless you have a strong reason for external index management\n\n## Event System\n\n### `onEvent` callback\n\n```tsx\nonEvent: (data: EventData, controls: Controls) => void\n```\n\nThe `data` object contains the full tour state plus event-specific fields. The `controls` object lets you programmatically control the tour.\n\n### Event types (in order per step)\n\n| Event | When |\n|-------|------|\n| `tour:start` | Tour begins |\n| `step:before_hook` | `before` hook is called |\n| `step:before` | Target found, step about to render |\n| `scroll:start` | Scrolling to target |\n| `scroll:end` | Scroll complete |\n| `beacon` | Beacon shown |\n| `tooltip` | Tooltip shown |\n| `step:after` | User navigated (next/prev/close/skip) |\n| `step:after_hook` | `after` hook called |\n| `tour:end` | Tour finished or skipped |\n| `tour:status` | Status changed (on stop/reset) |\n| `error:target_not_found` | Target element not found |\n| `error` | Generic error |\n\n### Event subscription with `on()`\n\n```tsx\nconst { on, Tour } = useJoyride({ ... });\n\nuseEffect(() => {\n  const unsubscribe = on('tooltip', (data, controls) => {\n    analytics.track('tour_step_viewed', { step: data.index });\n  });\n  return unsubscribe;\n}, [on]);\n```\n\nDocs: https://v3.react-joyride.com/docs/events\n\n## Controls\n\nAvailable via `useJoyride()` return value or `onEvent` second argument:\n\n| Method | Description |\n|--------|-------------|\n| `next()` | Advance to next step |\n| `prev()` | Go to previous step |\n| `close(origin?)` | Close current step, advance |\n| `skip(origin?)` | Skip the tour entirely |\n| `start(index?)` | Start the tour |\n| `stop(advance?)` | Stop (pause) the tour |\n| `go(index)` | Jump to step (uncontrolled only) |\n| `reset(restart?)` | Reset tour (uncontrolled only) |\n| `open()` | Open tooltip for current step |\n| `info()` | Get current state |\n\nDocs: https://v3.react-joyride.com/docs/hook\n\n## Styling & Theming\n\nThree layers of customization (from simple to full control):\n\n### 1. Color options (simplest)\n\n```tsx\noptions: {\n  primaryColor: '#1976d2',      // Buttons and beacon\n  backgroundColor: '#1a1a2e',   // Tooltip background\n  textColor: '#ffffff',         // Tooltip text\n  overlayColor: 'rgba(0,0,0,0.7)', // Overlay backdrop\n  arrowColor: '#1a1a2e',        // Arrow (match background)\n}\n```\n\n### 2. Styles override\n\n```tsx\nstyles: {\n  tooltip: { borderRadius: 12 },\n  buttonPrimary: { backgroundColor: '#1976d2' },\n  buttonBack: { color: '#666' },\n  spotlight: { borderRadius: 8 },\n}\n```\n\nStyle keys: `arrow`, `beacon`, `beaconInner`, `beaconOuter`, `beaconWrapper`, `buttonBack`, `buttonClose`, `buttonPrimary`, `buttonSkip`, `floater`, `loader`, `overlay`, `tooltip`, `tooltipContainer`, `tooltipContent`, `tooltipFooter`, `tooltipFooterSpacer`, `tooltipTitle`\n\n### 3. Custom components (full control)\n\nSee next section.\n\nDocs: https://v3.react-joyride.com/docs/props/styles\n\n## Custom Components\n\nReplace any UI component via props. Each receives render props with step data and button handlers.\n\n### Custom Tooltip\n\n```tsx\nimport type { TooltipRenderProps } from 'react-joyride';\n\nfunction MyTooltip({ backProps, index, primaryProps, size, skipProps, step, tooltipProps }: TooltipRenderProps) {\n  return (\n    <div {...tooltipProps} style={{ background: '#fff', padding: 16, borderRadius: 8, width: step.width }}>\n      {step.title && <h3>{step.title}</h3>}\n      <div>{step.content}</div>\n      <div>\n        {index > 0 && <button {...backProps}>Back</button>}\n        <button {...primaryProps}>Next</button>\n      </div>\n    </div>\n  );\n}\n\n// Usage\n<Joyride tooltipComponent={MyTooltip} ... />\n```\n\n**Important**: Spread `tooltipProps` on the container (sets `role=\"dialog\"` and `aria-modal`). Spread button props (`backProps`, `primaryProps`, `closeProps`, `skipProps`) on buttons for correct action handling.\n\n### Custom Beacon\n\nMust render a `<span>` (placed inside a `<button>` wrapper). Receives `BeaconRenderProps`: `{ continuous, index, isLastStep, size, step }`.\n\n### Custom Arrow\n\nReceives `ArrowRenderProps`: `{ base, placement, size }`.\n\n### Custom Loader\n\nReceives `LoaderRenderProps`: `{ step }`. Set to `null` to disable the loader.\n\nDocs: https://v3.react-joyride.com/docs/custom-components\n\n## Problem → Solution Guide\n\n| I need to... | Use |\n|---|---|\n| Wait for async UI changes between steps (dropdown, animation, data load) | `before` hook returning a Promise — not controlled mode |\n| Control step navigation externally (URL sync, external state machine) | Controlled mode with `stepIndex` — but try `before`/`after` hooks first |\n| Track which steps failed (target missing, hook error) | `failures` array from `useJoyride()` return |\n| Listen to specific events without `onEvent` switch | `on('event:type', handler)` from `useJoyride()` return |\n| Show a centered modal-style step | `target: 'body'` + `placement: 'center'` |\n\n## Common Gotchas & Debugging\n\n### Use `debug: true` first\n\nThe `debug` prop is the most powerful troubleshooting tool. It logs detailed lifecycle transitions, state changes, and event emissions to the console. Always start here when something isn't working.\n\n```tsx\n<Joyride debug={true} ... />\n// or\nuseJoyride({ debug: true, ... })\n```\n\nThe console output shows exactly which lifecycle phase the tour reaches, what actions are firing, and where it gets stuck.\n\n### Tour not starting\n- Check `run={true}` is set\n- Verify `steps` array is not empty and steps have valid `target` + `content`\n- For SSR: use `<Joyride>` component (auto-guards DOM access) or check `typeof window !== 'undefined'`\n\n### Target not found\n- Test selector in console: `document.querySelector('.your-selector')`\n- Element must be visible (not `display: none`, `visibility: hidden`, or zero dimensions)\n- If element mounts later, increase `targetWaitTimeout` (default: 1000ms)\n- Set `targetWaitTimeout: 0` to skip waiting entirely\n- In uncontrolled mode, missing targets auto-advance; in controlled mode, handle `error:target_not_found` event\n\n### Tooltip never appears / overlay flashes\n- Add `debug: true` and check the console to see which lifecycle phase is reached\n- Verify the target element is in the viewport or scrollable\n- Check for CSS `overflow: hidden` on ancestors clipping the target\n- If using portals or modals, the target may not be accessible\n\n### Controlled mode stuck\n- First question: do you actually need controlled mode? Most async needs are solved with `before`/`after` hooks in uncontrolled mode\n- If controlled: you MUST update `stepIndex` in your `onEvent` handler when `type === 'step:after'`\n- Handle both forward (`action !== 'prev'`) and backward (`action === 'prev'`) navigation\n- Also handle `error:target_not_found` to skip broken steps\n- `go()` and `reset()` don't work in controlled mode\n\n### Before hook timeout\n- Default `beforeTimeout` is 5000ms — increase if your async operation takes longer\n- Set `beforeTimeout: 0` for no timeout\n- The loader appears after `loaderDelay` (300ms) while waiting\n\n### Scroll issues\n- Use `scrollTarget` to scroll to a different element than the tooltip target\n- Adjust `scrollOffset` (default: 20px) for headers or fixed elements\n- Set `skipScroll: true` to disable auto-scrolling for a step\n- `scrollToFirstStep: false` by default — set to `true` if first step is off-screen\n\n### Center placement\n- Use `placement: 'center'` with `target: 'body'` for modal-style centered tooltips\n- Center placement automatically hides the beacon and arrow\n\n### Imports\n\n```tsx\n// Named exports only (no default export in v3)\nimport { Joyride, useJoyride } from 'react-joyride';\n\n// Constants for type-safe comparisons\nimport { ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS } from 'react-joyride';\n\n// Types\nimport type { Step, Props, EventData, Controls, TooltipRenderProps } from 'react-joyride';\n```\n\nDocs: https://v3.react-joyride.com/docs/exports\n\n## Reference Files\n\nRead these for complete API details:\n\n- `references/api-props-options.md` — Full Props, Options (all 30+ fields with defaults), Locale, FloatingOptions, Styles types\n- `references/api-step-state-controls.md` — Step, StepMerged, StepTarget, State, Controls (all 10 methods), UseJoyrideReturn, StepFailure\n- `references/api-events-components.md` — All 13 event types, ACTIONS/LIFECYCLE/STATUS/ORIGIN constants, EventData, custom component render props\n- `references/patterns.md` — Complete working examples: controlled mode, before/after hooks, custom tooltip, event subscription, dynamic steps\n\n<!-- File: CLAUDE.md (Tokens: ~1888 | Category: claude-rule) -->\n# CLAUDE.md\n\n## Overview\n\nReact Joyride is a React library for creating guided tours (tooltips, walkthroughs, onboarding). Supports React 16.8-19 and SSR.\n\nFor deep architecture details, see `docs/architecture.md`.\n\n## Commands\n\nPackage manager: **pnpm**\n\n```bash\npnpm lint              # ESLint with --fix on src/, test/, and e2e/\npnpm typecheck         # tsc using test/tsconfig.json\npnpm test              # Vitest (run once)\npnpm test:coverage     # With coverage\npnpm test:watch        # Watch mode\npnpm e2e               # Playwright (chromium, firefox, webkit)\npnpm e2e:chromium      # Chromium only\npnpm website           # Website at localhost:3000\npnpm website:build     # Build website for production\npnpm website:start     # Serve built website locally\npnpm website:serve     # Build + start website\npnpm build             # tsdown\npnpm check             # lint → typecheck → test:coverage\npnpm validate          # check → build → size → typevalidation\n```\n\nSingle test: `pnpm test modules/store` | Single e2e: `pnpm e2e:chromium standard`\n\n## How It Works\n\nSingleton `Store` class (ref-based) manages frozen state snapshots. React subscribes via `useSyncExternalStore`.\n\nTwo public APIs: `<Joyride>` component and `useJoyride()` hook (returns `{ controls, failures, on, state, step, Tour }`).\n\n**State machine** has two dimensions: tour `Status` (`idle → ready → waiting → running ↔ paused → finished/skipped`) and step `Lifecycle` (`init → ready → beacon_before → beacon → tooltip_before → tooltip → complete`). See architecture.md for full details.\n\n**Event system**: Single `onEvent(data, controls)` callback receives discriminated events and tour controls: `tour:start`, `step:before_hook`, `step:before`, `scroll:start`, `scroll:end`, `beacon`, `tooltip`, `step:after`, `step:after_hook`, `tour:end`, `tour:status`, `error:target_not_found`, `error`.\n\n**Controlled** (with `stepIndex` prop): Tour pauses at COMPLETE; parent manages index via `onEvent`. **Uncontrolled**: Store manages index internally; supports `initialStepIndex`.\n\n## Key Files\n\n| File | Role |\n|------|------|\n| `src/index.tsx` | SSR-safe wrapper, exports types and literals |\n| `src/hooks/useJoyride.tsx` | Public hook: orchestrates tour, returns controls/state/Tour |\n| `src/hooks/useTourEngine.ts` | Store init, subscriptions, delegates to sub-hooks |\n| `src/hooks/useControls.ts` | Tour control methods (next, prev, start, stop, etc.) |\n| `src/hooks/useEventEmitter.ts` | Centralized event emission with controls |\n| `src/hooks/usePropSync.ts` | Syncs run/steps props to store |\n| `src/hooks/useLifecycleEffect.ts` | 5 effects: action tracking, target resolution, presentation, transitions, tour flow |\n| `src/hooks/useScrollEffect.ts` | Scroll-to-target with placement adjustments |\n| `src/hooks/usePortalElement.ts` | Portal DOM element management |\n| `src/hooks/useTargetPosition.ts` | Target rect tracking via ResizeObserver |\n| `src/hooks/useFocusTrap.ts` | Focus trap for tooltip accessibility |\n| `src/modules/store.ts` | State machine + frozen snapshots |\n| `src/modules/dom.ts` | Element lookup, scroll, visibility |\n| `src/modules/step.ts` | Step merging + validation |\n| `src/modules/helpers.tsx` | Utilities (deepMerge, mergeProps, logDebug) |\n| `src/modules/changes.ts` | State transition detection (treeChanges) |\n| `src/modules/svg.ts` | SVG path generation for overlay/spotlight |\n| `src/defaults.ts` | Default props, locale, options |\n| `src/styles.ts` | Style generation from step |\n| `src/literals/index.ts` | Constants (ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS) |\n\n## Components\n\n| Component | File | Purpose |\n|-----------|------|---------|\n| TourRenderer | `src/components/TourRenderer.tsx` | Conditional rendering, keyboard handler (ESC) |\n| Step | `src/components/Step.tsx` | Validates step + target, renders Floater |\n| Floater | `src/components/Floater.tsx` | Dual Floating UI instances (beacon + tooltip), arrow |\n| Beacon | `src/components/Beacon.tsx` | Animated pulsing indicator |\n| Tooltip | `src/components/Tooltip/index.tsx` | Button handlers, custom component support |\n| DefaultTooltip | `src/components/Tooltip/DefaultTooltip.tsx` | Default tooltip layout |\n| CloseButton | `src/components/Tooltip/CloseButton.tsx` | SVG close icon |\n| Arrow | `src/components/Arrow.tsx` | Tooltip arrow, customizable via `arrowComponent` |\n| Overlay | `src/components/Overlay.tsx` | SVG-based spotlight with path cutout |\n| Loader | `src/components/Loader.tsx` | Loading indicator during target wait/before hooks |\n| Portal | `src/components/Portal.tsx` | React portal wrapper |\n\n## Types\n\nSplit into 8 domain files under `src/types/`: `common`, `components`, `events`, `floating`, `props`, `state`, `step`, `utilities`. Re-exported from `src/types/index.ts`.\n\n## Path Alias\n\n`~/*` maps to `src/*` (tsconfig.json; vitest via native `resolve.tsconfigPaths`; playwright uses website webServer).\n\n## Test Structure\n\n- **Unit tests** (Vitest + jsdom): `test/modules/`, `test/components/`, `test/hooks/`, `test/tours/`\n- **E2E tests** (Playwright): `e2e/` with screenshot snapshots per browser\n- **Fixtures**: `test/__fixtures__/` — Demo components used by both suites\n\nCoverage thresholds: 90% statements, 80% branches, 90% functions, 90% lines.\n\n## Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `@floating-ui/react-dom` | Tooltip/beacon positioning |\n| `@fastify/deepmerge` | Deep object merging (React element aware) |\n| `@gilbarbara/hooks` | useMemoDeepCompare, useMount, usePrevious, useUpdateEffect, useWindowSize |\n| `@gilbarbara/deep-equal` | Deep equality checking |\n| `scroll` / `scrollparent` | Smooth scrolling and scroll parent detection |\n| `react-innertext` | Extract text from React elements |\n| `is-lite` | Type checking utilities |\n\n## Website\n\nNext.js 16 documentation and demo site in `website/`. Imports library via local path (`\"react-joyride\": \"../\"`).\n\n**Stack**: HeroUI v2, Tailwind CSS v4, MDX (rehype-pretty-code + Shiki), next-themes, DocSearch.\n\n**Bundler**: `dev` runs on Turbopack; `build` stays on `--webpack` (`dev:webpack` is the fallback). `next build --turbopack` fails: `@next/mdx`'s Turbopack path never applies the `isServer` vendored-RSC-React alias (its `webpack()` hook does), so server-evaluated modules get the `react-server` React and any interactive HeroUI component's `@react-aria/ssr` `createContext` throws app-wide. The webpack alias in `next.config.ts` masks this; Turbopack has no server-only alias equivalent (a global `resolveAlias` pulls server React into the client bundle). Upstream: nextui-org/nextui#3967, shuding/nextra#4703.\n\n**Documentation** (`website/src/app/docs/`): MDX pages covering getting-started, new-in-v3, how-it-works, hook, props (options, styles, floating-options), step, events, custom-components, exports, accessibility, migration.\n\n**Demos** (`website/src/app/demos/`): 8 interactive examples — overview, controlled, custom-components, scroll, modal, carousel, chat, multi-route.\n\n**Key patterns**:\n- `ConfigContext` — centralized tour configuration for demos\n- `website/src/components/` — 33 components (ConfigPanel, custom tour components, code blocks, navigation)\n- `website/src/config/` — sidebar nav, default values, hero content\n\n## Visual Customization\n- Visual/cosmetic options (colors, borders, sizes) belong in `styles.*`, not as new Options fields.\n- Before adding a new option for appearance, check if it fits an existing or new `styles` key.\n</yakaai_skills>"}