{
  "name": "Tour",
  "category": "Overlay",
  "subcategory": null,
  "description": "Multi-step spotlight onboarding tour.",
  "thumbnail": "/preview-cards/tour.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/Tour/Tour.razor"
  ],
  "dependencies": [],
  "packageDependencies": [],
  "cssVars": [
    "--color-accent",
    "--color-background",
    "--color-border",
    "--color-foreground",
    "--color-muted",
    "--color-muted-foreground",
    "--color-popover",
    "--color-popover-foreground",
    "--color-primary",
    "--color-primary-foreground",
    "--radius"
  ],
  "api": {
    "name": "Tour",
    "category": "Overlay",
    "subcategory": null,
    "description": "Multi-step spotlight onboarding tour.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/Tour/Tour.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IAsyncDisposable"
    ],
    "parameters": [
      {
        "name": "Steps",
        "type": "List<TourStepConfig>",
        "default": "[]",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Open",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OpenChanged",
        "type": "EventCallback<bool>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "IsOpen",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "IsOpenChanged",
        "type": "EventCallback<bool>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "CurrentStep",
        "type": "int",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "CurrentStepChanged",
        "type": "EventCallback<int>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnComplete",
        "type": "EventCallback",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnSkip",
        "type": "EventCallback",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Class",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "AdditionalAttributes",
        "type": "Dictionary<string, object>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": true
      }
    ],
    "events": [
      {
        "name": "OpenChanged",
        "type": "EventCallback<bool>",
        "description": null
      },
      {
        "name": "IsOpenChanged",
        "type": "EventCallback<bool>",
        "description": null
      },
      {
        "name": "CurrentStepChanged",
        "type": "EventCallback<int>",
        "description": null
      },
      {
        "name": "OnComplete",
        "type": "EventCallback",
        "description": null
      },
      {
        "name": "OnSkip",
        "type": "EventCallback",
        "description": null
      }
    ],
    "enums": [
      {
        "name": "TourPlacement",
        "values": [
          "Top",
          "Bottom",
          "Left",
          "Right"
        ],
        "description": null
      }
    ],
    "records": [
      {
        "name": "TourStepConfig",
        "signature": "TourStepConfig(string? TargetSelector, string Title, string? Description = null, TourPlacement Placement = TourPlacement.Bottom)",
        "description": null
      }
    ],
    "cssVars": [
      "--color-accent",
      "--color-background",
      "--color-border",
      "--color-foreground",
      "--color-muted",
      "--color-muted-foreground",
      "--color-popover",
      "--color-popover-foreground",
      "--color-primary",
      "--color-primary-foreground",
      "--radius"
    ],
    "examples": [
      {
        "title": "Basic Tour",
        "code": "<Flex Class=\"gap-3 flex-wrap\">\n    <Button id=\"tour-feature-1\" Variant=\"Button.ButtonVariant.Outline\">Dashboard</Button>\n    <Button id=\"tour-feature-2\" Variant=\"Button.ButtonVariant.Outline\">Team</Button>\n    <Button id=\"tour-feature-3\" Variant=\"Button.ButtonVariant.Outline\">Settings</Button>\n</Flex>\n<Button Variant=\"Button.ButtonVariant.Default\" OnClick=\"@(() => _tourOpen = true)\">\n    Start Tour\n</Button>\n\n<Tour @bind-Open=\"_tourOpen\"\n      Steps=\"@_tourSteps\"\n      OnComplete=\"@HandleComplete\"\n      OnSkip=\"@(() => _tourOpen = false)\" />\n\n@code {\n    private bool _tourOpen;\n\n    private List<Tour.TourStepConfig> _tourSteps = new()\n    {\n        new(\"#tour-feature-1\", \"Dashboard\", \"Get an overview of your key metrics.\", Tour.TourPlacement.Bottom),\n        new(\"#tour-feature-2\", \"Team Management\", \"Invite members and manage permissions.\", Tour.TourPlacement.Bottom),\n        new(\"#tour-feature-3\", \"Settings\", \"Customize your workspace and account.\", Tour.TourPlacement.Bottom),\n    };\n\n    private void HandleComplete() => _tourOpen = false;\n}"
      },
      {
        "title": "Custom Placements",
        "code": "<Flex Class=\"justify-between gap-3\">\n    <Button id=\"tour-top\"    Variant=\"Button.ButtonVariant.Outline\">Top</Button>\n    <Button id=\"tour-right\"  Variant=\"Button.ButtonVariant.Outline\">Right</Button>\n    <Button id=\"tour-bottom\" Variant=\"Button.ButtonVariant.Outline\">Bottom</Button>\n    <Button id=\"tour-left\"   Variant=\"Button.ButtonVariant.Outline\">Left</Button>\n</Flex>\n<Button Variant=\"Button.ButtonVariant.Secondary\" OnClick=\"@(() => _open = true)\">\n    Tour Placements\n</Button>\n\n<Tour @bind-Open=\"_open\"\n      Steps=\"@_steps\"\n      OnComplete=\"@(() => _open = false)\"\n      OnSkip=\"@(() => _open = false)\" />\n\n@code {\n    private bool _open;\n\n    private List<Tour.TourStepConfig> _steps = new()\n    {\n        new(\"#tour-top\",    \"Top Placement\",    \"Tooltip above the element.\",  Tour.TourPlacement.Top),\n        new(\"#tour-right\",  \"Right Placement\",  \"Tooltip to the right.\",       Tour.TourPlacement.Right),\n        new(\"#tour-bottom\", \"Bottom Placement\", \"Tooltip below the element.\",  Tour.TourPlacement.Bottom),\n        new(\"#tour-left\",   \"Left Placement\",   \"Tooltip to the left.\",        Tour.TourPlacement.Left),\n    };\n}"
      }
    ],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "tour",
  "docsUrl": "https://lumeo.nativ.sh/components/tour",
  "sourceUrl": [
    {
      "file": "UI/Tour/Tour.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Tour/Tour.razor"
    }
  ],
  "source": [
    {
      "path": "UI/Tour/Tour.razor",
      "content": "@namespace Lumeo\n@implements IAsyncDisposable\n\n@if (Open && _currentStepConfig is not null)\n{\n    <!-- Spotlight overlay -->\n    <div class=\"fixed inset-0 z-[60]\" @onclick=\"HandleOverlayClick\">\n        <svg class=\"absolute inset-0 w-full h-full\" style=\"pointer-events: none;\">\n            <defs>\n                <mask id=\"@_maskId\">\n                    <rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" fill=\"white\" />\n                    @if (_targetRect is not null)\n                    {\n                        <rect x=\"@(_targetRect.X - 4)\" y=\"@(_targetRect.Y - 4)\"\n                              width=\"@(_targetRect.Width + 8)\" height=\"@(_targetRect.Height + 8)\"\n                              rx=\"@SpotlightRadius\" ry=\"@SpotlightRadius\" fill=\"black\" />\n                    }\n                </mask>\n            </defs>\n            <rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" fill=\"var(--color-overlay-backdrop)\"\n                  mask=\"url(#@_maskId)\" style=\"pointer-events: all;\" />\n        </svg>\n\n        @if (_targetRect is not null)\n        {\n            <!-- Highlight ring around target — border-radius matches the target's computed radius + the 4px spotlight padding. -->\n            <div class=\"absolute ring-2 ring-primary\" style=\"pointer-events: none;\n                left: @(_targetRect.X - 4)px; top: @(_targetRect.Y - 4)px;\n                width: @(_targetRect.Width + 8)px; height: @(_targetRect.Height + 8)px;\n                border-radius: @(SpotlightRadius)px;\" />\n        }\n    </div>\n\n    <!-- Tooltip -->\n    <div id=\"@_tooltipId\" class=\"@TooltipCssClass\" style=\"@TooltipStyle\" @attributes=\"AdditionalAttributes\">\n        <div class=\"flex flex-col gap-2\">\n            @if (!string.IsNullOrEmpty(_currentStepConfig.Title))\n            {\n                <h4 class=\"text-sm font-semibold text-foreground\">@_currentStepConfig.Title</h4>\n            }\n            @if (!string.IsNullOrEmpty(_currentStepConfig.Description))\n            {\n                <p class=\"text-sm text-muted-foreground\">@_currentStepConfig.Description</p>\n            }\n\n            <div class=\"flex items-center justify-between pt-2\">\n                <span class=\"text-xs text-muted-foreground\">@(CurrentStep + 1) / @Steps.Count</span>\n                <div class=\"flex items-center gap-2\">\n                    <button class=\"inline-flex items-center justify-center rounded-md text-xs font-medium h-7 px-2\n                                   text-muted-foreground hover:text-foreground transition-colors\"\n                            @onclick=\"HandleSkip\">\n                        @L[\"Tour.Skip\"]\n                    </button>\n                    @if (CurrentStep > 0)\n                    {\n                        <button class=\"inline-flex items-center justify-center rounded-md text-xs font-medium h-7 px-3\n                                       border border-border/60 bg-background text-foreground hover:bg-accent transition-colors\"\n                                @onclick=\"HandlePrevious\">\n                            @L[\"Tour.Previous\"]\n                        </button>\n                    }\n                    @if (CurrentStep < Steps.Count - 1)\n                    {\n                        <button class=\"inline-flex items-center justify-center rounded-md text-xs font-medium h-7 px-3\n                                       bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n                                @onclick=\"HandleNext\">\n                            @L[\"Tour.Next\"]\n                        </button>\n                    }\n                    else\n                    {\n                        <button class=\"inline-flex items-center justify-center rounded-md text-xs font-medium h-7 px-3\n                                       bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n                                @onclick=\"HandleComplete\">\n                            @L[\"Tour.Finish\"]\n                        </button>\n                    }\n                </div>\n            </div>\n        </div>\n    </div>\n}\n\n@inject ComponentInteropService Interop\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n@code {\n    [Parameter] public List<TourStepConfig> Steps { get; set; } = [];\n    [Parameter] public bool Open { get; set; }\n    [Parameter] public EventCallback<bool> OpenChanged { get; set; }\n\n    [Obsolete(\"Use Open instead. IsOpen will be removed in a future release.\")]\n    [Parameter] public bool IsOpen { get => Open; set => Open = value; }\n\n    [Obsolete(\"Use OpenChanged instead. IsOpenChanged will be removed in a future release.\")]\n    [Parameter] public EventCallback<bool> IsOpenChanged { get => OpenChanged; set => OpenChanged = value; }\n\n    [Parameter] public int CurrentStep { get; set; }\n    [Parameter] public EventCallback<int> CurrentStepChanged { get; set; }\n    [Parameter] public EventCallback OnComplete { get; set; }\n    [Parameter] public EventCallback OnSkip { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private readonly string _tooltipId = $\"tour-tooltip-{Guid.NewGuid():N}\";\n    private readonly string _maskId = $\"tour-mask-{Guid.NewGuid():N}\";\n    private ElementRect? _targetRect;\n    private TourStepConfig? _currentStepConfig;\n    private bool _scrollLocked;\n    private string? _currentTargetSelector;\n\n    private string TooltipCssClass => $\"fixed z-[61] w-80 rounded-lg border border-border/60 bg-popover p-4 text-popover-foreground shadow-lg animate-fade-in {Class}\".Trim();\n\n    // Spotlight padding (4 px all sides), plus the target's own border-radius, so the mask + ring wrap the target\n    // with the same corner curve they would have if you'd drawn them directly on top.\n    private string SpotlightRadius => ((_targetRect?.BorderRadius ?? 0) + 4).ToString(\"F2\", System.Globalization.CultureInfo.InvariantCulture);\n\n    private string TooltipStyle\n    {\n        get\n        {\n            if (_targetRect is null) return \"left: 50%; top: 50%; transform: translate(-50%, -50%);\";\n            var placement = _currentStepConfig?.Placement ?? TourPlacement.Bottom;\n            return placement switch\n            {\n                TourPlacement.Top => $\"left: {_targetRect.X + _targetRect.Width / 2}px; top: {_targetRect.Y - 12}px; transform: translateX(-50%) translateY(-100%);\",\n                TourPlacement.Bottom => $\"left: {_targetRect.X + _targetRect.Width / 2}px; top: {_targetRect.Y + _targetRect.Height + 12}px; transform: translateX(-50%);\",\n                TourPlacement.Left => $\"left: {_targetRect.X - 12}px; top: {_targetRect.Y + _targetRect.Height / 2}px; transform: translateX(-100%) translateY(-50%);\",\n                TourPlacement.Right => $\"left: {_targetRect.X + _targetRect.Width + 12}px; top: {_targetRect.Y + _targetRect.Height / 2}px; transform: translateY(-50%);\",\n                _ => $\"left: {_targetRect.X + _targetRect.Width / 2}px; top: {_targetRect.Y + _targetRect.Height + 12}px; transform: translateX(-50%);\"\n            };\n        }\n    }\n\n    protected override async Task OnParametersSetAsync()\n    {\n        if (Open && Steps.Count > 0 && CurrentStep >= 0 && CurrentStep < Steps.Count)\n        {\n            _currentStepConfig = Steps[CurrentStep];\n        }\n        else\n        {\n            _currentStepConfig = null;\n        }\n    }\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (Open && _currentStepConfig is not null)\n        {\n            if (!_scrollLocked)\n            {\n                await Interop.LockScroll();\n                _scrollLocked = true;\n            }\n            await UpdateTargetRect();\n        }\n        else if (_scrollLocked)\n        {\n            try { await Interop.UnlockScroll(); } catch (JSDisconnectedException) { }\n            _scrollLocked = false;\n        }\n    }\n\n    private async Task UpdateTargetRect()\n    {\n        var selector = _currentStepConfig?.TargetSelector;\n        if (selector == _currentTargetSelector) return; // Already positioned for this step\n        _currentTargetSelector = selector;\n        if (selector is not null)\n        {\n            _targetRect = await Interop.GetElementRectBySelector(selector);\n        }\n        else\n        {\n            _targetRect = null;\n        }\n        StateHasChanged(); // Safe: only fires when selector changes, not on every render\n    }\n\n    private async Task HandleNext()\n    {\n        if (CurrentStep < Steps.Count - 1)\n        {\n            CurrentStep++;\n            _currentStepConfig = Steps[CurrentStep];\n            _currentTargetSelector = null; // Force re-fetch for new step\n            await CurrentStepChanged.InvokeAsync(CurrentStep);\n            await UpdateTargetRect();\n        }\n    }\n\n    private async Task HandlePrevious()\n    {\n        if (CurrentStep > 0)\n        {\n            CurrentStep--;\n            _currentStepConfig = Steps[CurrentStep];\n            _currentTargetSelector = null; // Force re-fetch for new step\n            await CurrentStepChanged.InvokeAsync(CurrentStep);\n            await UpdateTargetRect();\n        }\n    }\n\n    private async Task HandleSkip()\n    {\n        await Close();\n        await OnSkip.InvokeAsync();\n    }\n\n    private async Task HandleComplete()\n    {\n        await Close();\n        await OnComplete.InvokeAsync();\n    }\n\n    private async Task HandleOverlayClick()\n    {\n        // Click on overlay (outside target) - skip tour\n        await HandleSkip();\n    }\n\n    private async Task Close()\n    {\n        Open = false;\n        CurrentStep = 0;\n        _currentStepConfig = null;\n        _targetRect = null;\n        _currentTargetSelector = null;\n        await OpenChanged.InvokeAsync(false);\n        await CurrentStepChanged.InvokeAsync(0);\n        if (_scrollLocked)\n        {\n            try { await Interop.UnlockScroll(); } catch (JSDisconnectedException) { }\n            _scrollLocked = false;\n        }\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        if (_scrollLocked)\n        {\n            try { await Interop.UnlockScroll(); } catch (JSDisconnectedException) { }\n        }\n    }\n\n    public record TourStepConfig(string? TargetSelector, string Title, string? Description = null, TourPlacement Placement = TourPlacement.Bottom);\n\n    public enum TourPlacement { Top, Bottom, Left, Right }\n}\n"
    }
  ],
  "slots": [],
  "serviceDependencies": [
    {
      "service": "ComponentInteropService",
      "namespace": ""
    },
    {
      "service": "ILumeoLocalizer",
      "namespace": "Lumeo.Services.Localization"
    }
  ],
  "cascadingDependencies": [],
  "relatedComponents": [],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Docs.Tests/Helpers/DocsTestContext.cs",
    "tests/Lumeo.Tests/Components/Tour/TourTests.cs",
    "tests/Lumeo.Tests/Contract/ComponentContractTests.cs"
  ],
  "mdSummary": "# Tour\n\nMulti-step spotlight onboarding tour.\n\n**Package:** `Lumeo` · **Category:** Overlay\n**Docs:** https://lumeo.nativ.sh/components/tour\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| Tour | Steps | `List<TourStepConfig>` | `[]` |  |\n| Tour | Open | `bool` | `` |  |\n| Tour | OpenChanged | `EventCallback<bool>` | `` |  |\n| Tour | IsOpen | `bool` | `` |  |\n| Tour | IsOpenChanged | `EventCallback<bool>` | `` |  |\n| Tour | CurrentStep | `int` | `` |  |\n| Tour | CurrentStepChanged | `EventCallback<int>` | `` |  |\n| Tour | OnComplete | `EventCallback` | `` |  |\n| Tour | OnSkip | `EventCallback` | `` |  |\n| Tour | Class | `string?` | `` |  |\n| Tour | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- (none)\n\n## Events\n- `OpenChanged` (`EventCallback<bool>`)\n- `IsOpenChanged` (`EventCallback<bool>`)\n- `CurrentStepChanged` (`EventCallback<int>`)\n- `OnComplete` (`EventCallback`)\n- `OnSkip` (`EventCallback`)\n\n## Sub-components\n- (none)\n\n## Cascading values it expects\n- (none)\n\n## Services it injects\n- `ComponentInteropService` ()\n- `ILumeoLocalizer` (Lumeo.Services.Localization)\n\n## Keyboard\n—\n\n## Example\n```razor\n<Tour />\n```\n\n## CSS variables\n`--color-accent`, `--color-background`, `--color-border`, `--color-foreground`, `--color-muted`, `--color-muted-foreground`, `--color-popover`, `--color-popover-foreground`, `--color-primary`, `--color-primary-foreground`, `--radius`\n\n## Files\n- `UI/Tour/Tour.razor`\n"
}