{
  "name": "Cascader",
  "category": "Forms",
  "subcategory": "Selection",
  "description": "Multi-level dropdown for hierarchical selection.",
  "thumbnail": "/preview-cards/cascader.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/Cascader/Cascader.razor"
  ],
  "dependencies": [
    "list"
  ],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-accent",
    "--color-accent-foreground",
    "--color-background",
    "--color-border",
    "--color-destructive",
    "--color-foreground",
    "--color-input",
    "--color-muted",
    "--color-muted-foreground",
    "--color-popover",
    "--color-popover-foreground",
    "--color-primary",
    "--color-ring",
    "--radius"
  ],
  "api": {
    "name": "Cascader",
    "category": "Forms",
    "subcategory": "Selection",
    "description": "Multi-level dropdown for hierarchical selection.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/Cascader/Cascader.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IAsyncDisposable"
    ],
    "parameters": [
      {
        "name": "FormField",
        "type": "FormField.FormFieldContext?",
        "default": null,
        "description": null,
        "isCascading": true,
        "captureUnmatched": false
      },
      {
        "name": "Value",
        "type": "List<string>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ValueChanged",
        "type": "EventCallback<List<string>?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Options",
        "type": "List<CascaderOption>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Placeholder",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Disabled",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Clearable",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ShowSearch",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Required",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Invalid",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ErrorText",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "HelperText",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Label",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Name",
        "type": "string?",
        "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": "ValueChanged",
        "type": "EventCallback<List<string>?>",
        "description": null
      }
    ],
    "enums": [],
    "records": [
      {
        "name": "CascaderOption",
        "signature": "CascaderOption",
        "description": null
      }
    ],
    "cssVars": [
      "--color-accent",
      "--color-accent-foreground",
      "--color-background",
      "--color-border",
      "--color-destructive",
      "--color-foreground",
      "--color-input",
      "--color-muted",
      "--color-muted-foreground",
      "--color-popover",
      "--color-popover-foreground",
      "--color-primary",
      "--color-ring",
      "--radius"
    ],
    "examples": [
      {
        "title": "Basic Cascader",
        "code": "<Cascader Options=\"@_options\" Placeholder=\"Select location...\" />"
      },
      {
        "title": "Clearable",
        "code": "<Cascader Options=\"@_options\" Clearable=\"true\" />"
      },
      {
        "title": "Disabled",
        "code": "<Cascader Options=\"@_options\" Disabled=\"true\" />"
      }
    ],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "cascader",
  "docsUrl": "https://lumeo.nativ.sh/components/cascader",
  "sourceUrl": [
    {
      "file": "UI/Cascader/Cascader.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Cascader/Cascader.razor"
    }
  ],
  "source": [
    {
      "path": "UI/Cascader/Cascader.razor",
      "content": "@namespace Lumeo\n@implements IAsyncDisposable\n\n@if (Label is not null && !InsideFormField)\n{\n    <label class=\"block text-sm font-medium text-foreground mb-1.5\">\n        @Label\n        @if (Required)\n        {\n            <span class=\"text-destructive ml-0.5\">*</span>\n        }\n    </label>\n}\n\n<div id=\"@_wrapperId\" class=\"@ContainerClass\" @attributes=\"AdditionalAttributes\">\n    @* Trigger *@\n    <button type=\"button\" class=\"@TriggerClass\" @onclick=\"ToggleOpen\" id=\"@_triggerId\" disabled=\"@Disabled\"\n            aria-required=\"@(Required ? \"true\" : \"false\")\"\n            aria-invalid=\"@(EffectiveInvalid ? \"true\" : \"false\")\">\n        @if (SelectedLabels.Count > 0)\n        {\n            <span class=\"truncate text-sm\">@string.Join(\" / \", SelectedLabels)</span>\n        }\n        else\n        {\n            <span class=\"text-sm text-muted-foreground\">@(Placeholder ?? L[\"Cascader.Placeholder\"])</span>\n        }\n\n        <span class=\"ml-auto flex items-center gap-1\">\n            @if (Clearable && Value is { Count: > 0 })\n            {\n                <button type=\"button\" class=\"rounded-full hover:bg-foreground/20 p-0.5\" aria-label=\"Clear selection\" @onclick=\"Clear\" @onclick:stopPropagation=\"true\">\n                    <Blazicon Svg=\"Lucide.X\" class=\"h-3.5 w-3.5 text-muted-foreground\" />\n                </button>\n            }\n            <Blazicon Svg=\"Lucide.ChevronDown\" class=\"@($\"h-4 w-4 text-muted-foreground transition-transform duration-200 {(_isOpen ? \"rotate-180\" : \"\")}\")\" />\n        </span>\n    </button>\n\n    @* Dropdown panels *@\n    @if (_isOpen)\n    {\n        <div id=\"@_contentId\" class=\"@ContentClass\" @onkeydown=\"HandleKeyDown\" tabindex=\"-1\">\n            @if (ShowSearch)\n            {\n                <div class=\"px-2 py-1.5 border-b border-border/40\">\n                    <input type=\"text\" placeholder=\"@L[\"Combobox.SearchPlaceholder\"]\"\n                           class=\"flex h-7 w-full rounded-md border border-input bg-transparent px-2 py-1 text-xs placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring\"\n                           value=\"@_searchText\"\n                           @oninput=\"OnSearchInput\" />\n                </div>\n            }\n\n            <div class=\"flex\">\n                @for (var levelIdx = 0; levelIdx <= _activePanels.Count; levelIdx++)\n                {\n                    var options = GetOptionsForLevel(levelIdx);\n                    if (options.Count == 0) break;\n\n                    var level = levelIdx;\n                    <div class=\"@PanelClass\">\n                        @foreach (var option in options)\n                        {\n                            var isSelected = level < _selectedPath.Count && _selectedPath[level] == option.Value;\n                            <button type=\"button\"\n                                    class=\"@GetOptionClass(isSelected)\"\n                                    @onclick=\"() => SelectOption(option, level)\">\n                                <span class=\"truncate\">@option.Label</span>\n                                @if (option.Children is { Count: > 0 })\n                                {\n                                    <Blazicon Svg=\"Lucide.ChevronRight\" class=\"h-4 w-4 text-muted-foreground ml-auto shrink-0\" />\n                                }\n                                else if (isSelected)\n                                {\n                                    <Blazicon Svg=\"Lucide.Check\" class=\"h-4 w-4 text-primary ml-auto shrink-0\" />\n                                }\n                            </button>\n                        }\n                    </div>\n                }\n            </div>\n        </div>\n    }\n</div>\n\n@if (!InsideFormField)\n{\n    @if (EffectiveInvalid && ErrorText is not null)\n    {\n        <p class=\"text-xs text-destructive mt-1.5\">@ErrorText</p>\n    }\n    else if (HelperText is not null)\n    {\n        <p class=\"text-xs text-muted-foreground mt-1.5\">@HelperText</p>\n    }\n}\n\n@inject ComponentInteropService Interop\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n@code {\n    [CascadingParameter] private FormField.FormFieldContext? FormField { get; set; }\n\n    [Parameter] public List<string>? Value { get; set; }\n    [Parameter] public EventCallback<List<string>?> ValueChanged { get; set; }\n    [Parameter] public List<CascaderOption>? Options { get; set; }\n    [Parameter] public string? Placeholder { get; set; }\n    [Parameter] public bool Disabled { get; set; }\n    [Parameter] public bool Clearable { get; set; }\n    [Parameter] public bool ShowSearch { get; set; }\n    [Parameter] public bool Required { get; set; }\n    [Parameter] public bool Invalid { get; set; }\n    [Parameter] public string? ErrorText { get; set; }\n    [Parameter] public string? HelperText { get; set; }\n    [Parameter] public string? Label { get; set; }\n    [Parameter] public string? Name { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private bool EffectiveInvalid => Invalid || FormField?.HasError == true;\n    private bool InsideFormField => FormField is not null;\n\n    private bool _isOpen;\n    private bool _registered;\n    private string _searchText = \"\";\n    private readonly List<string> _selectedPath = [];\n    private readonly List<CascaderOption> _activePanels = [];\n    private readonly string _wrapperId = $\"cascader-{Guid.NewGuid():N}\";\n    private readonly string _triggerId = $\"cascader-trigger-{Guid.NewGuid():N}\";\n    private readonly string _contentId = $\"cascader-content-{Guid.NewGuid():N}\";\n\n    private string ContainerClass => $\"relative inline-block {Class}\".Trim();\n    private string TriggerClass => $\"flex items-center w-full h-9 px-3 rounded-md border {(EffectiveInvalid ? \"border-destructive focus:ring-destructive/20\" : \"border-input\")} bg-background text-sm hover:bg-accent/50 transition-colors disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-[1.5px] {(EffectiveInvalid ? \"focus:ring-destructive/20\" : \"focus:ring-ring\")}\";\n    private const string ContentClass = \"fixed z-50 overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-lg animate-fade-in\";\n    private const string PanelClass = \"min-w-[160px] max-h-64 overflow-y-auto border-r border-border/40 last:border-r-0 p-1\";\n\n    private List<string> SelectedLabels\n    {\n        get\n        {\n            if (Value is not { Count: > 0 } || Options is null) return [];\n            var labels = new List<string>();\n            var current = Options;\n            foreach (var val in Value)\n            {\n                var opt = current.FirstOrDefault(o => o.Value == val);\n                if (opt is null) break;\n                labels.Add(opt.Label);\n                current = opt.Children ?? [];\n            }\n            return labels;\n        }\n    }\n\n    protected override void OnParametersSet()\n    {\n        if (Value is { Count: > 0 })\n        {\n            _selectedPath.Clear();\n            _activePanels.Clear();\n            var current = Options ?? [];\n            foreach (var val in Value)\n            {\n                var opt = current.FirstOrDefault(o => o.Value == val);\n                if (opt is null) break;\n                _selectedPath.Add(val);\n                if (opt.Children is { Count: > 0 })\n                {\n                    _activePanels.Add(opt);\n                    current = opt.Children;\n                }\n            }\n        }\n    }\n\n    private List<CascaderOption> GetOptionsForLevel(int level)\n    {\n        if (level == 0)\n        {\n            var opts = Options ?? [];\n            return string.IsNullOrWhiteSpace(_searchText)\n                ? opts\n                : SearchOptions(opts, _searchText);\n        }\n\n        if (level - 1 < _activePanels.Count)\n        {\n            return _activePanels[level - 1].Children ?? [];\n        }\n\n        return [];\n    }\n\n    private static List<CascaderOption> SearchOptions(List<CascaderOption> options, string search)\n    {\n        var result = new List<CascaderOption>();\n        foreach (var opt in options)\n        {\n            if (opt.Label.Contains(search, StringComparison.OrdinalIgnoreCase))\n            {\n                result.Add(opt);\n            }\n            else if (opt.Children is { Count: > 0 })\n            {\n                var childMatches = SearchOptions(opt.Children, search);\n                if (childMatches.Count > 0) result.Add(opt);\n            }\n        }\n        return result;\n    }\n\n    private async Task SelectOption(CascaderOption option, int level)\n    {\n        // Trim path and panels to current level\n        while (_selectedPath.Count > level) _selectedPath.RemoveAt(_selectedPath.Count - 1);\n        while (_activePanels.Count > level) _activePanels.RemoveAt(_activePanels.Count - 1);\n\n        _selectedPath.Add(option.Value);\n\n        if (option.Children is { Count: > 0 })\n        {\n            _activePanels.Add(option);\n        }\n        else\n        {\n            // Leaf node — set value and close\n            Value = [.._selectedPath];\n            await ValueChanged.InvokeAsync(Value);\n            await SetOpen(false);\n        }\n    }\n\n    private async Task ToggleOpen() => await SetOpen(!_isOpen);\n\n    private async Task SetOpen(bool value)\n    {\n        var wasOpen = _isOpen;\n        _isOpen = value;\n        if (!value) _searchText = \"\";\n\n        // On close: return focus to the trigger button immediately (it is in the DOM).\n        if (wasOpen && !value)\n        {\n            try { await Interop.FocusElement(_triggerId); } catch (JSDisconnectedException) { }\n        }\n        // On open: focus happens in OnAfterRenderAsync once the panel DOM is ready.\n    }\n\n    private async Task Clear()\n    {\n        Value = null;\n        _selectedPath.Clear();\n        _activePanels.Clear();\n        await ValueChanged.InvokeAsync(null);\n    }\n\n    private async Task HandleKeyDown(KeyboardEventArgs e)\n    {\n        if (e.Key == \"Escape\") await SetOpen(false);\n    }\n\n    private void OnSearchInput(ChangeEventArgs e)\n    {\n        _searchText = e.Value?.ToString() ?? \"\";\n    }\n\n    private static string GetOptionClass(bool isSelected) =>\n        $\"flex w-full items-center gap-1 rounded-sm px-2 py-1.5 text-sm cursor-pointer transition-colors {(isSelected ? \"bg-accent text-accent-foreground\" : \"hover:bg-accent/50\")}\";\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (_isOpen && !_registered)\n        {\n            await Interop.PositionFixed(_contentId, _triggerId, \"start\", false, \"bottom\");\n            await Interop.RegisterClickOutside(_contentId, _triggerId, async () =>\n            {\n                await SetOpen(false);\n                await InvokeAsync(StateHasChanged);\n            });\n            _registered = true;\n\n            // Focus the first option in the first panel so keyboard users can\n            // immediately navigate without an extra Tab press.\n            try { await Interop.FocusMenuItemByIndex(_contentId, 0); } catch (JSDisconnectedException) { }\n        }\n        else if (!_isOpen && _registered)\n        {\n            await Cleanup();\n        }\n    }\n\n    private async Task Cleanup()\n    {\n        try { await Interop.UnpositionFixed(_contentId); } catch (JSDisconnectedException) { }\n        try { await Interop.UnregisterClickOutside(_contentId); } catch (JSDisconnectedException) { }\n        _registered = false;\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        if (_registered) await Cleanup();\n    }\n\n    public record CascaderOption\n    {\n        public string Label { get; init; } = \"\";\n        public string Value { get; init; } = \"\";\n        public List<CascaderOption>? Children { get; init; }\n    }\n}\n"
    }
  ],
  "slots": [],
  "serviceDependencies": [
    {
      "service": "ComponentInteropService",
      "namespace": ""
    },
    {
      "service": "ILumeoLocalizer",
      "namespace": "Lumeo.Services.Localization"
    }
  ],
  "cascadingDependencies": [
    {
      "name": "FormField",
      "type": "FormField.FormFieldContext?",
      "required": false
    }
  ],
  "relatedComponents": [
    {
      "name": "List",
      "reason": "used internally"
    }
  ],
  "keyboardInteractions": [
    {
      "key": "Escape",
      "action": "HandleKeyDown handles Escape in Cascader.razor"
    }
  ],
  "tests": [
    "tests/Lumeo.Tests/Components/Cascader/CascaderTests.cs"
  ],
  "mdSummary": "# Cascader\n\nMulti-level dropdown for hierarchical selection.\n\n**Package:** `Lumeo` · **Category:** Forms/Selection\n**Docs:** https://lumeo.nativ.sh/components/cascader\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| Cascader | FormField | `FormField.FormFieldContext?` | `` |  |\n| Cascader | Value | `List<string>?` | `` |  |\n| Cascader | ValueChanged | `EventCallback<List<string>?>` | `` |  |\n| Cascader | Options | `List<CascaderOption>?` | `` |  |\n| Cascader | Placeholder | `string?` | `` |  |\n| Cascader | Disabled | `bool` | `` |  |\n| Cascader | Clearable | `bool` | `` |  |\n| Cascader | ShowSearch | `bool` | `` |  |\n| Cascader | Required | `bool` | `` |  |\n| Cascader | Invalid | `bool` | `` |  |\n| Cascader | ErrorText | `string?` | `` |  |\n| Cascader | HelperText | `string?` | `` |  |\n| Cascader | Label | `string?` | `` |  |\n| Cascader | Name | `string?` | `` |  |\n| Cascader | Class | `string?` | `` |  |\n| Cascader | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- (none)\n\n## Events\n- `ValueChanged` (`EventCallback<List<string>?>`)\n\n## Sub-components\n- (none)\n\n## Cascading values it expects\n- `FormField` (`FormField.FormFieldContext?`)\n\n## Services it injects\n- `ComponentInteropService` ()\n- `ILumeoLocalizer` (Lumeo.Services.Localization)\n\n## Keyboard\n- `Escape` — HandleKeyDown handles Escape in Cascader.razor\n\n## Example\n```razor\n<Cascader />\n```\n\n## CSS variables\n`--color-accent`, `--color-accent-foreground`, `--color-background`, `--color-border`, `--color-destructive`, `--color-foreground`, `--color-input`, `--color-muted`, `--color-muted-foreground`, `--color-popover`, `--color-popover-foreground`, `--color-primary`, `--color-ring`, `--radius`\n\n## Files\n- `UI/Cascader/Cascader.razor`\n"
}