{
  "name": "TreeSelect",
  "category": "Forms",
  "subcategory": "Selection",
  "description": "Select input with a hierarchical tree dropdown.",
  "thumbnail": "/preview-cards/tree-select.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/TreeSelect/TreeSelect.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-primary",
    "--color-ring",
    "--radius"
  ],
  "api": {
    "name": "TreeSelect",
    "category": "Forms",
    "subcategory": "Selection",
    "description": "Select input with a hierarchical tree dropdown.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/TreeSelect/TreeSelect.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IAsyncDisposable"
    ],
    "parameters": [
      {
        "name": "FormField",
        "type": "FormField.FormFieldContext?",
        "default": null,
        "description": null,
        "isCascading": true,
        "captureUnmatched": false
      },
      {
        "name": "Value",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ValueChanged",
        "type": "EventCallback<string>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Items",
        "type": "List<TreeSelectItem>",
        "default": "new()",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Placeholder",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Searchable",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Multiple",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Values",
        "type": "List<string>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ValuesChanged",
        "type": "EventCallback<List<string>?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Disabled",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ExpandAll",
        "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<string>",
        "description": null
      },
      {
        "name": "ValuesChanged",
        "type": "EventCallback<List<string>?>",
        "description": null
      }
    ],
    "enums": [],
    "records": [],
    "cssVars": [
      "--color-accent",
      "--color-accent-foreground",
      "--color-background",
      "--color-border",
      "--color-destructive",
      "--color-foreground",
      "--color-input",
      "--color-muted",
      "--color-muted-foreground",
      "--color-popover",
      "--color-primary",
      "--color-ring",
      "--radius"
    ],
    "examples": [
      {
        "title": "Basic Tree Select",
        "code": "<TreeSelect @bind-Value=\"_selected\" Items=\"@_categories\" />\n\n@code {\n    private string? _selected;\n    private List<TreeSelect.TreeSelectItem> _categories = new()\n    {\n        new() { Label = \"Electronics\", Value = \"electronics\", Children = new()\n        {\n            new() { Label = \"Phones\", Value = \"phones\" },\n            new() { Label = \"Computers\", Value = \"computers\" },\n        }},\n        new() { Label = \"Books\", Value = \"books\" },\n    };\n}"
      },
      {
        "title": "Searchable",
        "code": "<TreeSelect @bind-Value=\"_selected\" Items=\"@_categories\" Searchable=\"true\" />"
      },
      {
        "title": "Multi-select",
        "code": "<TreeSelect Values=\"@_values\" ValuesChanged=\"@(v => _values = v)\"\n            Items=\"@_categories\" Multiple=\"true\" Searchable=\"true\" />"
      },
      {
        "title": "Expand All",
        "code": "<TreeSelect @bind-Value=\"_selected\" Items=\"@_categories\" ExpandAll=\"true\" />"
      }
    ],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "tree-select",
  "docsUrl": "https://lumeo.nativ.sh/components/tree-select",
  "sourceUrl": [
    {
      "file": "UI/TreeSelect/TreeSelect.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/TreeSelect/TreeSelect.razor"
    }
  ],
  "source": [
    {
      "path": "UI/TreeSelect/TreeSelect.razor",
      "content": "@namespace Lumeo\n@implements IAsyncDisposable\n\n@{\n    var displayText = GetDisplayText();\n}\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=\"relative inline-block @Class\" @attributes=\"AdditionalAttributes\">\n    <button type=\"button\"\n            class=\"@TriggerClass\"\n            @onclick=\"ToggleOpen\"\n            disabled=\"@Disabled\"\n            aria-haspopup=\"tree\"\n            aria-expanded=\"@_isOpen\"\n            aria-required=\"@(Required ? \"true\" : \"false\")\"\n            aria-invalid=\"@(EffectiveInvalid ? \"true\" : \"false\")\">\n        <span class=\"@(string.Join(\" \", new[] { \"flex-1 truncate text-left\", displayText == (Placeholder ?? L[\"TreeSelect.Placeholder\"]) ? \"text-muted-foreground\" : null }.Where(s => !string.IsNullOrWhiteSpace(s))))\">@displayText</span>\n        @{ var chevronClass = string.Join(\" \", new[] { \"h-4 w-4 shrink-0 opacity-50 transition-transform\", _isOpen ? \"rotate-180\" : null }.Where(s => !string.IsNullOrWhiteSpace(s))); }\n        <Blazicon Svg=\"Lucide.ChevronDown\" class=\"@chevronClass\" />\n    </button>\n\n    @if (_isOpen)\n    {\n        <div id=\"@_contentId\"\n             class=\"fixed z-50 min-w-[12rem] overflow-hidden rounded-md border border-border bg-popover shadow-lg animate-fade-in\"\n             role=\"tree\">\n            @if (Searchable)\n            {\n                <div class=\"p-2 border-b border-border/40\">\n                    <input type=\"text\"\n                           class=\"w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm outline-none placeholder:text-muted-foreground focus:ring-1 focus:ring-ring\"\n                           placeholder=\"@L[\"Combobox.SearchPlaceholder\"]\"\n                           value=\"@_searchText\"\n                           @oninput=\"OnSearchInput\"\n                           @onclick:stopPropagation=\"true\" />\n                </div>\n            }\n            <div class=\"max-h-60 overflow-y-auto p-1\">\n                @foreach (var item in Items)\n                {\n                    <div @key=\"item.Value\">@RenderTreeItem(item, 0)</div>\n                }\n                @if (!HasVisibleItems())\n                {\n                    <div class=\"py-4 text-center text-sm text-muted-foreground\">@L[\"TreeSelect.NoResults\"]</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 string? Value { get; set; }\n    [Parameter] public EventCallback<string> ValueChanged { get; set; }\n    [Parameter] public List<TreeSelectItem> Items { get; set; } = new();\n    [Parameter] public string? Placeholder { get; set; }\n    [Parameter] public bool Searchable { get; set; }\n    [Parameter] public bool Multiple { get; set; }\n    [Parameter] public List<string>? Values { get; set; }\n    [Parameter] public EventCallback<List<string>?> ValuesChanged { get; set; }\n    [Parameter] public bool Disabled { get; set; }\n    [Parameter] public bool ExpandAll { 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 readonly string _wrapperId = $\"treeselect-{Guid.NewGuid():N}\";\n    private readonly string _contentId = $\"treeselect-content-{Guid.NewGuid():N}\";\n    private bool _isOpen;\n    private bool _registered;\n    private bool _disposed;\n    private string _searchText = \"\";\n    private readonly HashSet<string> _expandedNodes = new();\n\n    private string TriggerClass =>\n        $\"flex h-9 w-full items-center justify-between rounded-md border {(EffectiveInvalid ? \"border-destructive\" : \"border-input\")} bg-background px-3 py-2 text-sm shadow-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50 hover:bg-accent/50 focus:outline-none focus:ring-1 {(EffectiveInvalid ? \"focus:ring-destructive/20\" : \"focus:ring-ring\")}\".Trim();\n\n    protected override void OnInitialized()\n    {\n        if (ExpandAll)\n        {\n            ExpandAllNodes(Items);\n        }\n    }\n\n    private void ExpandAllNodes(List<TreeSelectItem> items)\n    {\n        foreach (var item in items)\n        {\n            if (item.Children?.Count > 0)\n            {\n                _expandedNodes.Add(item.Value);\n                ExpandAllNodes(item.Children);\n            }\n        }\n    }\n\n    private string GetDisplayText()\n    {\n        var placeholder = Placeholder ?? L[\"TreeSelect.Placeholder\"];\n        if (Multiple)\n        {\n            if (Values == null || Values.Count == 0) return placeholder;\n            var labels = Values.Select(v => FindLabel(Items, v)).Where(l => l != null).ToList();\n            return labels.Count > 0 ? string.Join(\", \", labels) : placeholder;\n        }\n        if (string.IsNullOrEmpty(Value)) return placeholder;\n        return FindLabel(Items, Value) ?? placeholder;\n    }\n\n    private string? FindLabel(List<TreeSelectItem> items, string value)\n    {\n        foreach (var item in items)\n        {\n            if (item.Value == value) return item.Label;\n            if (item.Children?.Count > 0)\n            {\n                var found = FindLabel(item.Children, value);\n                if (found != null) return found;\n            }\n        }\n        return null;\n    }\n\n    private bool HasVisibleItems() => Items.Any(i => IsVisible(i));\n\n    private bool IsVisible(TreeSelectItem item)\n    {\n        if (string.IsNullOrEmpty(_searchText)) return true;\n        if (item.Label.Contains(_searchText, StringComparison.OrdinalIgnoreCase)) return true;\n        return item.Children?.Any(IsVisible) == true;\n    }\n\n    private bool MatchesSearch(TreeSelectItem item) =>\n        string.IsNullOrEmpty(_searchText) ||\n        item.Label.Contains(_searchText, StringComparison.OrdinalIgnoreCase);\n\n    private bool IsSelected(string value)\n    {\n        if (Multiple) return Values?.Contains(value) == true;\n        return Value == value;\n    }\n\n    private async Task SelectItem(TreeSelectItem item)\n    {\n        if (item.Disabled) return;\n\n        if (Multiple)\n        {\n            var current = Values ?? new List<string>();\n            if (current.Contains(item.Value))\n                current = current.Where(v => v != item.Value).ToList();\n            else\n                current = new List<string>(current) { item.Value };\n            Values = current.Count > 0 ? current : null;\n            await ValuesChanged.InvokeAsync(Values);\n        }\n        else\n        {\n            Value = item.Value;\n            await ValueChanged.InvokeAsync(item.Value);\n            await CloseDropdown();\n        }\n    }\n\n    private void ToggleNode(string value)\n    {\n        if (_expandedNodes.Contains(value))\n            _expandedNodes.Remove(value);\n        else\n            _expandedNodes.Add(value);\n    }\n\n    private async Task ToggleOpen()\n    {\n        if (_isOpen)\n            await CloseDropdown();\n        else\n            await OpenDropdown();\n    }\n\n    private async Task OpenDropdown()\n    {\n        _isOpen = true;\n        _searchText = \"\";\n        StateHasChanged();\n        await Task.Yield();\n        if (_disposed) return;\n        await Interop.PositionFixed(_contentId, _wrapperId, \"start\", true, \"bottom\");\n        if (_disposed) return;\n        await Interop.RegisterClickOutside(_contentId, _wrapperId, async () =>\n        {\n            await CloseDropdown();\n            await InvokeAsync(StateHasChanged);\n        });\n        if (_disposed) return;\n        _registered = true;\n    }\n\n    private async Task CloseDropdown()\n    {\n        _isOpen = false;\n        if (_registered)\n        {\n            try { await Interop.UnpositionFixed(_contentId); } catch (JSDisconnectedException) { }\n            try { await Interop.UnregisterClickOutside(_contentId); } catch (JSDisconnectedException) { }\n            _registered = false;\n        }\n    }\n\n    private void OnSearchInput(ChangeEventArgs e)\n    {\n        _searchText = e.Value?.ToString() ?? \"\";\n        if (!string.IsNullOrEmpty(_searchText))\n        {\n            // Auto-expand all when searching\n            ExpandAllNodes(Items);\n        }\n    }\n\n    private RenderFragment RenderTreeItem(TreeSelectItem item, int depth) => __builder =>\n    {\n        if (!IsVisible(item)) return;\n\n        var hasChildren = item.Children?.Count > 0;\n        var isExpanded = _expandedNodes.Contains(item.Value);\n        var isSelected = IsSelected(item.Value);\n        var isLeaf = !hasChildren;\n        var paddingLeft = depth * 16 + 8;\n\n        <div role=\"treeitem\" aria-selected=\"@isSelected\" aria-expanded=\"@(hasChildren ? isExpanded : null)\">\n            <div class=\"@GetItemClass(isSelected, item.Disabled, isLeaf)\"\n                 style=\"padding-left: @(paddingLeft)px\"\n                 @onclick=\"() => HandleItemClick(item, hasChildren)\"\n                 @onclick:stopPropagation=\"true\">\n                @if (hasChildren)\n                {\n                    <button type=\"button\"\n                            class=\"mr-1 rounded hover:bg-accent p-0.5\"\n                            @onclick=\"() => ToggleNode(item.Value)\"\n                            @onclick:stopPropagation=\"true\">\n                        @{ var chevronRightClass = string.Join(\" \", new[] { \"h-3.5 w-3.5 transition-transform\", isExpanded ? \"rotate-90\" : null }.Where(s => !string.IsNullOrWhiteSpace(s))); }\n                        <Blazicon Svg=\"Lucide.ChevronRight\" class=\"@chevronRightClass\" />\n                    </button>\n                }\n                else\n                {\n                    <span class=\"mr-1 w-5 shrink-0\"></span>\n                }\n\n                @if (!string.IsNullOrEmpty(item.Icon))\n                {\n                    <span class=\"mr-1.5 text-muted-foreground text-xs\">@item.Icon</span>\n                }\n\n                <span class=\"flex-1 truncate text-sm\">@item.Label</span>\n\n                @if (isSelected && isLeaf)\n                {\n                    <Blazicon Svg=\"Lucide.Check\" class=\"h-4 w-4 text-primary shrink-0\" />\n                }\n            </div>\n\n            @if (hasChildren && isExpanded)\n            {\n                <div role=\"group\">\n                    @foreach (var child in item.Children!)\n                    {\n                        <div @key=\"child.Value\">@RenderTreeItem(child, depth + 1)</div>\n                    }\n                </div>\n            }\n        </div>\n    };\n\n    private async Task HandleItemClick(TreeSelectItem item, bool hasChildren)\n    {\n        if (hasChildren)\n            ToggleNode(item.Value);\n        else\n            await SelectItem(item);\n    }\n\n    private string GetItemClass(bool isSelected, bool disabled, bool isLeaf) =>\n        string.Join(\" \", new[] {\n            \"flex items-center gap-1 rounded-md py-1.5 pr-2 cursor-pointer transition-colors\",\n            disabled ? \"opacity-50 cursor-not-allowed\" : isSelected && isLeaf ? \"bg-accent text-accent-foreground\" : \"hover:bg-accent/60\"\n        }.Where(s => !string.IsNullOrWhiteSpace(s)));\n\n    public async ValueTask DisposeAsync()\n    {\n        _disposed = true;\n        if (_registered)\n        {\n            try { await Interop.UnpositionFixed(_contentId); } catch (JSDisconnectedException) { }\n            try { await Interop.UnregisterClickOutside(_contentId); } catch (JSDisconnectedException) { }\n        }\n    }\n\n    public class TreeSelectItem\n    {\n        public string Label { get; set; } = \"\";\n        public string Value { get; set; } = \"\";\n        public List<TreeSelectItem>? Children { get; set; }\n        public bool Disabled { get; set; }\n        public string? Icon { get; set; }\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": [],
  "tests": [
    "tests/Lumeo.Tests/Components/TreeSelect/TreeSelectTests.cs"
  ],
  "mdSummary": "# TreeSelect\n\nSelect input with a hierarchical tree dropdown.\n\n**Package:** `Lumeo` · **Category:** Forms/Selection\n**Docs:** https://lumeo.nativ.sh/components/tree-select\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| TreeSelect | FormField | `FormField.FormFieldContext?` | `` |  |\n| TreeSelect | Value | `string?` | `` |  |\n| TreeSelect | ValueChanged | `EventCallback<string>` | `` |  |\n| TreeSelect | Items | `List<TreeSelectItem>` | `new()` |  |\n| TreeSelect | Placeholder | `string?` | `` |  |\n| TreeSelect | Searchable | `bool` | `` |  |\n| TreeSelect | Multiple | `bool` | `` |  |\n| TreeSelect | Values | `List<string>?` | `` |  |\n| TreeSelect | ValuesChanged | `EventCallback<List<string>?>` | `` |  |\n| TreeSelect | Disabled | `bool` | `` |  |\n| TreeSelect | ExpandAll | `bool` | `` |  |\n| TreeSelect | Required | `bool` | `` |  |\n| TreeSelect | Invalid | `bool` | `` |  |\n| TreeSelect | ErrorText | `string?` | `` |  |\n| TreeSelect | HelperText | `string?` | `` |  |\n| TreeSelect | Label | `string?` | `` |  |\n| TreeSelect | Name | `string?` | `` |  |\n| TreeSelect | Class | `string?` | `` |  |\n| TreeSelect | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- (none)\n\n## Events\n- `ValueChanged` (`EventCallback<string>`)\n- `ValuesChanged` (`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—\n\n## Example\n```razor\n<TreeSelect />\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-primary`, `--color-ring`, `--radius`\n\n## Files\n- `UI/TreeSelect/TreeSelect.razor`\n"
}