{
  "name": "Combobox",
  "category": "Forms",
  "subcategory": "Selection",
  "description": "Searchable select with filtering, custom values, and grouping.",
  "thumbnail": "/preview-cards/combobox.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/Combobox/Combobox.razor",
    "UI/Combobox/ComboboxContent.razor",
    "UI/Combobox/ComboboxCreate.razor",
    "UI/Combobox/ComboboxEmpty.razor",
    "UI/Combobox/ComboboxInput.razor",
    "UI/Combobox/ComboboxItem.razor"
  ],
  "dependencies": [
    "spinner"
  ],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-accent",
    "--color-accent-foreground",
    "--color-border",
    "--color-destructive",
    "--color-input",
    "--color-muted",
    "--color-muted-foreground",
    "--color-popover",
    "--color-popover-foreground",
    "--radius"
  ],
  "api": {
    "name": "Combobox",
    "category": "Forms",
    "subcategory": "Selection",
    "description": "Searchable select with filtering, custom values, and grouping.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/Combobox/Combobox.razor",
      "UI/Combobox/ComboboxContent.razor",
      "UI/Combobox/ComboboxCreate.razor",
      "UI/Combobox/ComboboxEmpty.razor",
      "UI/Combobox/ComboboxInput.razor",
      "UI/Combobox/ComboboxItem.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IDisposable"
    ],
    "parameters": [
      {
        "name": "FormField",
        "type": "FormField.FormFieldContext?",
        "default": null,
        "description": null,
        "isCascading": true,
        "captureUnmatched": false
      },
      {
        "name": "ChildContent",
        "type": "RenderFragment?",
        "default": null,
        "description": null,
        "isCascading": false,
        "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": "Open",
        "type": "bool",
        "default": null,
        "description": "Popover open state. Bind via @bind-Open when the Combobox lives inside a parent that re-renders heavily (e.g. an EditForm with active validation) — otherwise a parameter cascade can close the popover mid-click. In the bound (controlled) mode the parent owns state and the popover survives re-renders. When unbound, the component still functions but is vulnerable to the cascade-close race; binding is the recommended pattern.",
        "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": "AdditionalAttributes",
        "type": "Dictionary<string, object>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": true
      },
      {
        "name": "Multiple",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Values",
        "type": "HashSet<string>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ValuesChanged",
        "type": "EventCallback<HashSet<string>>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Creatable",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnCreate",
        "type": "EventCallback<string>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnSearchAsync",
        "type": "EventCallback<string>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "DebounceMs",
        "type": "int",
        "default": "300",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "IsSearchLoading",
        "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": "Items",
        "type": "IEnumerable<object>?",
        "default": null,
        "description": "Optional data source. When set, ComboboxContent renders options from this list instead of ChildContent.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ItemValue",
        "type": "Func<object, string>",
        "default": "it => it?.ToString() ?? \"\"",
        "description": "Extracts the string value for each item. Defaults to ToString().",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ItemText",
        "type": "Func<object, string>?",
        "default": null,
        "description": "Extracts the display label for each item. Defaults to ItemValue when null.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ItemGroup",
        "type": "Func<object, string?>?",
        "default": null,
        "description": "Optional group key extractor. When set, options are grouped under sticky headers.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ItemDisabled",
        "type": "Func<object, bool>?",
        "default": null,
        "description": "Optional per-item disabled predicate.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ItemTemplate",
        "type": "RenderFragment<object>?",
        "default": null,
        "description": "Optional custom render fragment for an item's content. Receives the raw item object.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Virtualize",
        "type": "bool",
        "default": null,
        "description": "When true and Items is set, uses Blazor's Virtualize component for the option list.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ItemSize",
        "type": "float",
        "default": "36f",
        "description": "Estimated pixel height of each option row used by the Virtualize component. Default 36.",
        "isCascading": false,
        "captureUnmatched": false
      }
    ],
    "events": [
      {
        "name": "ValueChanged",
        "type": "EventCallback<string>",
        "description": null
      },
      {
        "name": "OpenChanged",
        "type": "EventCallback<bool>",
        "description": null
      },
      {
        "name": "IsOpenChanged",
        "type": "EventCallback<bool>",
        "description": null
      },
      {
        "name": "ValuesChanged",
        "type": "EventCallback<HashSet<string>>",
        "description": null
      },
      {
        "name": "OnCreate",
        "type": "EventCallback<string>",
        "description": null
      },
      {
        "name": "OnSearchAsync",
        "type": "EventCallback<string>",
        "description": null
      }
    ],
    "enums": [],
    "records": [
      {
        "name": "ComboboxContext",
        "signature": "ComboboxContext(\n        string? Value, HashSet<string>? Values, string SearchText, bool IsOpen, bool Multiple, bool IsSearchLoading, bool Creatable,\n        EventCallback<string> OnSelect, EventCallback<bool> SetOpen, EventCallback<string> OnSearch,\n        EventCallback<string> OnCreateItem,\n        string WrapperId, string ContentId, int FocusedIndex, string? FocusedItemId, string? FocusedItemValue,\n        EventCallback<int> SetFocusedIndex,\n        Action<string, string> RegisterItem, Action<string> UnregisterItem, int ItemCount,\n        bool Invalid, bool Required,\n        // Data-bound mode params (null when composition mode)\n        IEnumerable<object>? Items,\n        Func<object, string> ItemValue,\n        Func<object, string>? ItemText,\n        Func<object, string?>? ItemGroup,\n        Func<object, bool>? ItemDisabled,\n        RenderFragment<object>? ItemTemplate,\n        bool Virtualize,\n        float ItemSize)",
        "description": null
      }
    ],
    "cssVars": [
      "--color-accent",
      "--color-accent-foreground",
      "--color-border",
      "--color-destructive",
      "--color-input",
      "--color-muted",
      "--color-muted-foreground",
      "--color-popover",
      "--color-popover-foreground",
      "--radius"
    ],
    "examples": [
      {
        "title": "Default",
        "code": "<Combobox @bind-Value=\"_selected\" @bind-Open=\"_isOpen\">\n    <ComboboxInput Placeholder=\"Search frameworks...\" />\n    <ComboboxContent>\n        <ComboboxItem Value=\"blazor\">Blazor</ComboboxItem>\n        <ComboboxItem Value=\"react\">React</ComboboxItem>\n        <ComboboxItem Value=\"vue\">Vue.js</ComboboxItem>\n        <ComboboxEmpty>No framework found.</ComboboxEmpty>\n    </ComboboxContent>\n</Combobox>"
      },
      {
        "title": "Country Selector",
        "code": "<Label>Country</Label>\n<Combobox @bind-Value=\"_selected\" @bind-Open=\"_isOpen\">\n    <ComboboxInput Placeholder=\"Search countries...\" />\n    <ComboboxContent>\n        <ComboboxItem Value=\"Australia\">Australia</ComboboxItem>\n        <ComboboxItem Value=\"Canada\">Canada</ComboboxItem>\n        <ComboboxItem Value=\"France\">France</ComboboxItem>\n        <ComboboxItem Value=\"United Kingdom\">United Kingdom</ComboboxItem>\n        <ComboboxItem Value=\"United States\">United States</ComboboxItem>\n        <ComboboxEmpty>No country found.</ComboboxEmpty>\n    </ComboboxContent>\n</Combobox>"
      },
      {
        "title": "With Label",
        "code": "<Label>Language</Label>\n<Combobox @bind-Value=\"_selected\" @bind-Open=\"_isOpen\">\n    <ComboboxInput Placeholder=\"Search languages...\" />\n    <ComboboxContent>\n        <ComboboxItem Value=\"C#\">C#</ComboboxItem>\n        <ComboboxItem Value=\"TypeScript\">TypeScript</ComboboxItem>\n        <ComboboxEmpty>No language found.</ComboboxEmpty>\n    </ComboboxContent>\n</Combobox>"
      },
      {
        "title": "Multiple Selection",
        "code": "<Combobox Multiple=\"true\" @bind-Values=\"_selectedFrameworks\" @bind-Open=\"_isOpen\">\n    <ComboboxInput Placeholder=\"Select frameworks...\" />\n    <ComboboxContent>\n        <ComboboxItem Value=\"react\">React</ComboboxItem>\n        <ComboboxItem Value=\"vue\">Vue</ComboboxItem>\n        <ComboboxItem Value=\"angular\">Angular</ComboboxItem>\n        <ComboboxItem Value=\"svelte\">Svelte</ComboboxItem>\n        <ComboboxItem Value=\"blazor\">Blazor</ComboboxItem>\n    </ComboboxContent>\n</Combobox>\n\n@code {\n    private HashSet<string> _selectedFrameworks = new();\n    private bool _isOpen;\n}"
      }
    ],
    "subComponents": {
      "ComboboxContent": {
        "componentName": "ComboboxContent",
        "fileName": "ComboboxContent.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [
          "IAsyncDisposable"
        ],
        "parameters": [
          {
            "name": "Context",
            "type": "Combobox.ComboboxContext",
            "default": "default!",
            "description": null,
            "isCascading": true,
            "captureUnmatched": false
          },
          {
            "name": "ChildContent",
            "type": "RenderFragment?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "EmptyText",
            "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": [],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      },
      "ComboboxCreate": {
        "componentName": "ComboboxCreate",
        "fileName": "ComboboxCreate.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [],
        "parameters": [
          {
            "name": "Context",
            "type": "Combobox.ComboboxContext",
            "default": "default!",
            "description": null,
            "isCascading": true,
            "captureUnmatched": false
          },
          {
            "name": "OnCreate",
            "type": "EventCallback<string>",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "ShowCreate",
            "type": "bool",
            "default": "true",
            "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": "OnCreate",
            "type": "EventCallback<string>",
            "description": null
          }
        ],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      },
      "ComboboxEmpty": {
        "componentName": "ComboboxEmpty",
        "fileName": "ComboboxEmpty.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [],
        "parameters": [
          {
            "name": "Context",
            "type": "Combobox.ComboboxContext?",
            "default": null,
            "description": null,
            "isCascading": true,
            "captureUnmatched": false
          },
          {
            "name": "ChildContent",
            "type": "RenderFragment?",
            "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": [],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      },
      "ComboboxInput": {
        "componentName": "ComboboxInput",
        "fileName": "ComboboxInput.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [],
        "parameters": [
          {
            "name": "Context",
            "type": "Combobox.ComboboxContext",
            "default": "default!",
            "description": null,
            "isCascading": true,
            "captureUnmatched": false
          },
          {
            "name": "Placeholder",
            "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": [],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      },
      "ComboboxItem": {
        "componentName": "ComboboxItem",
        "fileName": "ComboboxItem.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [
          "IDisposable"
        ],
        "parameters": [
          {
            "name": "Context",
            "type": "Combobox.ComboboxContext",
            "default": "default!",
            "description": null,
            "isCascading": true,
            "captureUnmatched": false
          },
          {
            "name": "ChildContent",
            "type": "RenderFragment?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Value",
            "type": "string",
            "default": "\"\"",
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Class",
            "type": "string?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "SearchValue",
            "type": "string?",
            "default": null,
            "description": "Optional text used to match against the search input. Falls back to Value.",
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "AdditionalAttributes",
            "type": "Dictionary<string, object>?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": true
          }
        ],
        "events": [],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      }
    },
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "combobox",
  "docsUrl": "https://lumeo.nativ.sh/components/combobox",
  "sourceUrl": [
    {
      "file": "UI/Combobox/Combobox.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Combobox/Combobox.razor"
    },
    {
      "file": "UI/Combobox/ComboboxContent.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Combobox/ComboboxContent.razor"
    },
    {
      "file": "UI/Combobox/ComboboxCreate.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Combobox/ComboboxCreate.razor"
    },
    {
      "file": "UI/Combobox/ComboboxEmpty.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Combobox/ComboboxEmpty.razor"
    },
    {
      "file": "UI/Combobox/ComboboxInput.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Combobox/ComboboxInput.razor"
    },
    {
      "file": "UI/Combobox/ComboboxItem.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Combobox/ComboboxItem.razor"
    }
  ],
  "source": [
    {
      "path": "UI/Combobox/Combobox.razor",
      "content": "@namespace Lumeo\n@implements IDisposable\n\n@{\n    var focusedItemId = _focusedIndex >= 0 && _focusedIndex < _items.Count ? _items[_focusedIndex].Id : null;\n    var focusedItemValue = _focusedIndex >= 0 && _focusedIndex < _items.Count ? _items[_focusedIndex].Value : null;\n    var context = new ComboboxContext(Value, Values, SearchText, Open, Multiple, IsSearchLoading, Creatable,\n        EventCallback.Factory.Create<string>(this, OnValueSelected),\n        EventCallback.Factory.Create<bool>(this, SetOpen),\n        EventCallback.Factory.Create<string>(this, OnSearchChanged),\n        EventCallback.Factory.Create<string>(this, OnCreateItem),\n        _wrapperId, _contentId, _focusedIndex, focusedItemId, focusedItemValue,\n        EventCallback.Factory.Create<int>(this, SetFocusedIndex),\n        RegisterItem, UnregisterItem, _items.Count, EffectiveInvalid, Required,\n        Items, ItemValue, ItemText, ItemGroup, ItemDisabled, ItemTemplate, Virtualize, ItemSize);\n}\n\n@if (Label is not null && !InsideFormField)\n{\n    <label class=\"block text-sm font-medium mb-1.5\">\n        @Label\n        @if (Required)\n        {\n            <span class=\"text-destructive ml-0.5\">*</span>\n        }\n    </label>\n}\n\n<CascadingValue Value=\"context\" IsFixed=\"false\">\n    <div id=\"@_wrapperId\" class=\"relative\" @attributes=\"AdditionalAttributes\">\n        @ChildContent\n    </div>\n</CascadingValue>\n\n@if (!InsideFormField)\n{\n    @if (EffectiveInvalid && !string.IsNullOrEmpty(ErrorText))\n    {\n        <p class=\"mt-1 text-xs text-destructive\">@ErrorText</p>\n    }\n    else if (!string.IsNullOrEmpty(HelperText))\n    {\n        <p class=\"mt-1 text-xs text-muted-foreground\">@HelperText</p>\n    }\n}\n\n@code {\n    [CascadingParameter] private FormField.FormFieldContext? FormField { get; set; }\n\n    [Parameter] public RenderFragment? ChildContent { get; set; }\n    [Parameter] public string? Value { get; set; }\n    [Parameter] public EventCallback<string> ValueChanged { get; set; }\n    /// <summary>\n    /// Popover open state. Bind via <c>@bind-Open</c> when the Combobox lives inside\n    /// a parent that re-renders heavily (e.g. an <c>EditForm</c> with active\n    /// validation) — otherwise a parameter cascade can close the popover mid-click.\n    /// In the bound (controlled) mode the parent owns state and the popover survives\n    /// re-renders. When unbound, the component still functions but is vulnerable to\n    /// the cascade-close race; binding is the recommended pattern.\n    /// </summary>\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(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    // Multi-select\n    [Parameter] public bool Multiple { get; set; }\n    [Parameter] public HashSet<string>? Values { get; set; }\n    [Parameter] public EventCallback<HashSet<string>> ValuesChanged { get; set; }\n\n    // Creatable\n    [Parameter] public bool Creatable { get; set; }\n    [Parameter] public EventCallback<string> OnCreate { get; set; }\n\n    // Async search\n    [Parameter] public EventCallback<string> OnSearchAsync { get; set; }\n    [Parameter] public int DebounceMs { get; set; } = 300;\n    [Parameter] public bool IsSearchLoading { get; set; }\n\n    // Validation\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\n    // --- Data-bound mode (non-generic, purely additive) ---\n    /// <summary>Optional data source. When set, <see cref=\"ComboboxContent\"/> renders options from this list instead of ChildContent.</summary>\n    [Parameter] public IEnumerable<object>? Items { get; set; }\n    /// <summary>Extracts the string value for each item. Defaults to <c>ToString()</c>.</summary>\n    [Parameter] public Func<object, string> ItemValue { get; set; } = it => it?.ToString() ?? \"\";\n    /// <summary>Extracts the display label for each item. Defaults to <see cref=\"ItemValue\"/> when null.</summary>\n    [Parameter] public Func<object, string>? ItemText { get; set; }\n    /// <summary>Optional group key extractor. When set, options are grouped under sticky headers.</summary>\n    [Parameter] public Func<object, string?>? ItemGroup { get; set; }\n    /// <summary>Optional per-item disabled predicate.</summary>\n    [Parameter] public Func<object, bool>? ItemDisabled { get; set; }\n    /// <summary>Optional custom render fragment for an item's content. Receives the raw item object.</summary>\n    [Parameter] public RenderFragment<object>? ItemTemplate { get; set; }\n    /// <summary>When <c>true</c> and <see cref=\"Items\"/> is set, uses Blazor's Virtualize component for the option list.</summary>\n    [Parameter] public bool Virtualize { get; set; }\n    /// <summary>Estimated pixel height of each option row used by the Virtualize component. Default 36.</summary>\n    [Parameter] public float ItemSize { get; set; } = 36f;\n\n    private bool EffectiveInvalid => Invalid || FormField?.HasError == true;\n    private bool InsideFormField => FormField is not null;\n\n    private readonly string _wrapperId = $\"combobox-{Guid.NewGuid():N}\";\n    private readonly string _contentId = $\"combobox-content-{Guid.NewGuid():N}\";\n    private string SearchText { get; set; } = \"\";\n    private readonly DelayedDispatch _dispatch = new();\n    private int _focusedIndex = -1;\n    private readonly List<(string Id, string Value)> _items = new();\n\n    private void RegisterItem(string id, string value)\n    {\n        if (!_items.Any(i => i.Id == id))\n            _items.Add((id, value));\n    }\n\n    private void UnregisterItem(string id)\n    {\n        _items.RemoveAll(i => i.Id == id);\n    }\n\n    private async Task SetFocusedIndex(int index)\n    {\n        var count = _items.Count;\n        if (count == 0) return;\n        if (index < 0) index = 0;\n        if (index >= count) index = count - 1;\n        _focusedIndex = index;\n        StateHasChanged();\n        await Task.CompletedTask;\n    }\n\n    private async Task OnValueSelected(string value)\n    {\n        if (Multiple)\n        {\n            Values ??= [];\n            if (Values.Contains(value))\n                Values.Remove(value);\n            else\n                Values.Add(value);\n            await ValuesChanged.InvokeAsync(Values);\n        }\n        else\n        {\n            Value = value;\n            await ValueChanged.InvokeAsync(value);\n            await SetOpen(false);\n        }\n    }\n\n    private async Task SetOpen(bool value)\n    {\n        Open = value;\n        if (!value && !Multiple)\n        {\n            SearchText = \"\";\n            _focusedIndex = -1;\n            _items.Clear();\n        }\n        await OpenChanged.InvokeAsync(value);\n    }\n\n    private async Task OnSearchChanged(string text)\n    {\n        SearchText = text;\n        _focusedIndex = -1;\n\n        // Only clear registered items for async search — async replaces the item set, so the old registrations\n        // are stale. For static items (filtered by the parent's bound text), the items remain mounted and handle\n        // their own register/unregister lifecycle; clearing here mis-reports ItemCount as 0 and triggers the\n        // \"no results\" empty state while items are still visible.\n        if (OnSearchAsync.HasDelegate)\n        {\n            _items.Clear();\n            _dispatch.Schedule(DebounceMs, () => InvokeAsync(async () =>\n            {\n                await OnSearchAsync.InvokeAsync(text);\n                StateHasChanged();\n            }));\n        }\n\n        await Task.CompletedTask;\n    }\n\n    private async Task OnCreateItem(string value)\n    {\n        await OnCreate.InvokeAsync(value);\n        if (Multiple)\n        {\n            Values ??= [];\n            Values.Add(value);\n            await ValuesChanged.InvokeAsync(Values);\n        }\n        else\n        {\n            Value = value;\n            await ValueChanged.InvokeAsync(value);\n            await SetOpen(false);\n        }\n        SearchText = \"\";\n    }\n\n    public void Dispose() => _dispatch.Dispose();\n\n    public record ComboboxContext(\n        string? Value, HashSet<string>? Values, string SearchText, bool IsOpen, bool Multiple, bool IsSearchLoading, bool Creatable,\n        EventCallback<string> OnSelect, EventCallback<bool> SetOpen, EventCallback<string> OnSearch,\n        EventCallback<string> OnCreateItem,\n        string WrapperId, string ContentId, int FocusedIndex, string? FocusedItemId, string? FocusedItemValue,\n        EventCallback<int> SetFocusedIndex,\n        Action<string, string> RegisterItem, Action<string> UnregisterItem, int ItemCount,\n        bool Invalid, bool Required,\n        // Data-bound mode params (null when composition mode)\n        IEnumerable<object>? Items,\n        Func<object, string> ItemValue,\n        Func<object, string>? ItemText,\n        Func<object, string?>? ItemGroup,\n        Func<object, bool>? ItemDisabled,\n        RenderFragment<object>? ItemTemplate,\n        bool Virtualize,\n        float ItemSize);\n}\n"
    },
    {
      "path": "UI/Combobox/ComboboxContent.razor",
      "content": "@namespace Lumeo\n@implements IAsyncDisposable\n@using Microsoft.AspNetCore.Components.Web.Virtualization\n\n@if (Context.IsOpen)\n{\n    <div id=\"@Context.ContentId\" role=\"listbox\" class=\"@CssClass\"\n         @onkeydown=\"HandleKeyDown\" tabindex=\"-1\" @attributes=\"AdditionalAttributes\">\n        @if (Context.IsSearchLoading)\n        {\n            <div class=\"flex items-center justify-center py-2 px-2 text-sm text-muted-foreground\">\n                <Spinner Class=\"h-4 w-4 mr-2\" />\n                @L[\"Combobox.Loading\"]\n            </div>\n        }\n        @if (Context.Items is not null)\n        {\n            @* Data-bound mode *@\n            var filteredItems = FilteredItems;\n            @if (Context.Virtualize)\n            {\n                <Virtualize Items=\"@filteredItems\" Context=\"item\" ItemSize=\"@Context.ItemSize\">\n                    @RenderBoundItem(item)\n                </Virtualize>\n            }\n            else if (Context.ItemGroup is not null)\n            {\n                @foreach (var group in filteredItems.GroupBy(i => Context.ItemGroup(i) ?? \"\"))\n                {\n                    <div role=\"group\">\n                        @if (!string.IsNullOrEmpty(group.Key))\n                        {\n                            <div class=\"px-2 py-1.5 text-sm font-semibold\">@group.Key</div>\n                        }\n                        @foreach (var item in group)\n                        {\n                            @RenderBoundItem(item)\n                        }\n                    </div>\n                }\n            }\n            else\n            {\n                @foreach (var item in filteredItems)\n                {\n                    @RenderBoundItem(item)\n                }\n            }\n\n            @if (filteredItems.Count == 0 && !Context.IsSearchLoading)\n            {\n                <div class=\"py-6 text-center text-sm text-muted-foreground\">\n                    @(EmptyText ?? L[\"Combobox.NoResults\"])\n                </div>\n            }\n\n            @if (Context.Creatable && !string.IsNullOrWhiteSpace(Context.SearchText) && filteredItems.Count == 0)\n            {\n                <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 px-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground border-t border-border/40 mt-1 pt-2\"\n                        @onclick=\"HandleCreate\">\n                    <Blazicon Svg=\"Lucide.Plus\" class=\"h-3.5 w-3.5\" />\n                    <span>@string.Format(L[\"Combobox.Create\"], Context.SearchText)</span>\n                </button>\n            }\n        }\n        else\n        {\n            @* Composition mode *@\n            @ChildContent\n            @if (!Context.IsSearchLoading && Context.ItemCount == 0 && ChildContent is null)\n            {\n                @* Fallback default empty — only renders if the consumer didn't supply any children at all.\n                   The real empty state is `<ComboboxEmpty>` which renders itself when ItemCount hits 0. *@\n                <div class=\"py-6 text-center text-sm text-muted-foreground\">\n                    @(EmptyText ?? L[\"Combobox.NoResults\"])\n                </div>\n            }\n            @if (Context.Creatable && !string.IsNullOrWhiteSpace(Context.SearchText) && Context.ItemCount == 0)\n            {\n                <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 px-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground border-t border-border/40 mt-1 pt-2\"\n                        @onclick=\"HandleCreate\">\n                    <Blazicon Svg=\"Lucide.Plus\" class=\"h-3.5 w-3.5\" />\n                    <span>@string.Format(L[\"Combobox.Create\"], Context.SearchText)</span>\n                </button>\n            }\n        }\n    </div>\n}\n\n@inject ComponentInteropService Interop\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n@code {\n    [CascadingParameter] public Combobox.ComboboxContext Context { get; set; } = default!;\n    [Parameter] public RenderFragment? ChildContent { get; set; }\n    [Parameter] public string? EmptyText { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private bool _registered;\n\n    private string CssClass => $\"fixed z-50 overflow-hidden rounded-md border border-border/60 bg-popover p-1 text-popover-foreground shadow-lg animate-fade-in {Class}\".Trim();\n\n    // Items filtered by the current search text (data-bound mode only).\n    //\n    // Previously a computed property: at 10k items it ran the Where+ToList for\n    // every access, and the markup above accesses it 3+ times per render\n    // (Virtualize Items, foreach, Count checks). Now we memoize: recompute\n    // only when the Items reference changes OR the search text changes.\n    // Invalidation happens in OnParametersSet — any external state mutation\n    // that flows through a parameter pass (which is the only way Combobox\n    // updates its descendants) gets picked up.\n    // Keep the concrete type List<object> — the markup uses .Count and passes\n    // this list to <Virtualize Items=...> which wants an ICollection<T>.\n    private List<object> _filteredItems = new();\n    private IEnumerable<object>? _lastItems;\n    private string? _lastSearchText;\n\n    private List<object> FilteredItems => _filteredItems;\n\n    protected override void OnParametersSet()\n    {\n        var items = Context?.Items;\n        var search = Context?.SearchText;\n\n        if (!ReferenceEquals(items, _lastItems) || !string.Equals(search, _lastSearchText, StringComparison.Ordinal))\n        {\n            _lastItems = items;\n            _lastSearchText = search;\n            _filteredItems = ComputeFiltered(items, search);\n        }\n    }\n\n    private List<object> ComputeFiltered(IEnumerable<object>? items, string? searchText)\n    {\n        if (items is null) return new();\n        if (string.IsNullOrEmpty(searchText))\n            return items.ToList();\n\n        return items.Where(item =>\n        {\n            var text = Context.ItemText?.Invoke(item) ?? Context.ItemValue(item);\n            return text.Contains(searchText, StringComparison.OrdinalIgnoreCase);\n        }).ToList();\n    }\n\n    private RenderFragment RenderBoundItem(object item) => builder =>\n    {\n        var value = Context.ItemValue(item);\n        var isSelected = Context.Multiple\n            ? Context.Values?.Contains(value) == true\n            : Context.Value == value;\n        var isDisabled = Context.ItemDisabled?.Invoke(item) ?? false;\n        var itemId = $\"combobox-item-bound-{value.GetHashCode():X}\";\n        var isFocused = Context.FocusedItemId == itemId;\n\n        builder.OpenElement(0, \"button\");\n        builder.AddAttribute(1, \"type\", \"button\");\n        builder.AddAttribute(2, \"id\", itemId);\n        builder.AddAttribute(3, \"role\", \"option\");\n        builder.AddAttribute(4, \"aria-selected\", isSelected.ToString().ToLower());\n        var cssClass = string.Join(\" \", new[]\n        {\n            \"relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground\",\n            isFocused ? \"bg-accent text-accent-foreground\" : null\n        }.Where(s => !string.IsNullOrWhiteSpace(s)));\n        builder.AddAttribute(5, \"class\", cssClass);\n        if (!isDisabled)\n        {\n            builder.AddAttribute(6, \"onclick\", EventCallback.Factory.Create(this, () => Context.OnSelect.InvokeAsync(value)));\n        }\n        builder.OpenElement(7, \"span\");\n        builder.AddAttribute(8, \"class\", \"flex h-3.5 w-3.5 items-center justify-center\");\n        if (isSelected)\n        {\n            builder.OpenComponent<Blazicons.Blazicon>(9);\n            builder.AddAttribute(10, \"Svg\", Blazicons.Lucide.Check);\n            builder.AddAttribute(11, \"class\", \"h-3.5 w-3.5\");\n            builder.CloseComponent();\n        }\n        builder.CloseElement(); // span\n\n        if (Context.ItemTemplate is not null)\n        {\n            builder.AddContent(12, Context.ItemTemplate(item));\n        }\n        else\n        {\n            var label = Context.ItemText?.Invoke(item) ?? value;\n            builder.AddContent(12, label);\n        }\n        builder.CloseElement(); // button\n    };\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (Context.IsOpen && !_registered)\n        {\n            await Interop.PositionFixed(Context.ContentId, Context.WrapperId, \"start\", true);\n            await Interop.RegisterClickOutside(Context.ContentId, null, async () =>\n            {\n                await Context.SetOpen.InvokeAsync(false);\n                await InvokeAsync(StateHasChanged);\n            });\n            _registered = true;\n        }\n        else if (!Context.IsOpen && _registered)\n        {\n            await Cleanup();\n        }\n    }\n\n    private async Task HandleKeyDown(KeyboardEventArgs e)\n    {\n        switch (e.Key)\n        {\n            case \"Escape\":\n                await Context.SetOpen.InvokeAsync(false);\n                break;\n            case \"ArrowDown\":\n                await Context.SetFocusedIndex.InvokeAsync(Context.FocusedIndex + 1);\n                break;\n            case \"ArrowUp\":\n                await Context.SetFocusedIndex.InvokeAsync(Context.FocusedIndex > 0 ? Context.FocusedIndex - 1 : 0);\n                break;\n            case \"Home\":\n                await Context.SetFocusedIndex.InvokeAsync(0);\n                break;\n            case \"End\":\n                await Context.SetFocusedIndex.InvokeAsync(int.MaxValue);\n                break;\n            case \"Enter\":\n                if (Context.FocusedItemValue is not null)\n                    await Context.OnSelect.InvokeAsync(Context.FocusedItemValue);\n                break;\n        }\n    }\n\n    private async Task HandleCreate()\n    {\n        await Context.OnCreateItem.InvokeAsync(Context.SearchText);\n    }\n\n    private async Task Cleanup()\n    {\n        try { await Interop.UnpositionFixed(Context.ContentId); } catch (JSDisconnectedException) { }\n        try { await Interop.UnregisterClickOutside(Context.ContentId); } catch (JSDisconnectedException) { }\n        _registered = false;\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        if (_registered) await Cleanup();\n    }\n}\n"
    },
    {
      "path": "UI/Combobox/ComboboxCreate.razor",
      "content": "@namespace Lumeo\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n@if (!string.IsNullOrWhiteSpace(Context.SearchText) && ShowCreate)\n{\n    <button type=\"button\" class=\"@CssClass\" @onclick=\"HandleCreate\" @attributes=\"AdditionalAttributes\">\n        <Blazicon Svg=\"Lucide.Plus\" class=\"h-3.5 w-3.5 mr-2\" />\n        @string.Format(L[\"Combobox.Create\"], Context.SearchText)\n    </button>\n}\n\n@code {\n    [CascadingParameter] public Combobox.ComboboxContext Context { get; set; } = default!;\n    [Parameter] public EventCallback<string> OnCreate { get; set; }\n    [Parameter] public bool ShowCreate { get; set; } = true;\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private string CssClass => $\"relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 px-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground border-t border-border/40 mt-1 pt-2 {Class}\".Trim();\n\n    private async Task HandleCreate()\n    {\n        await OnCreate.InvokeAsync(Context.SearchText);\n    }\n}\n"
    },
    {
      "path": "UI/Combobox/ComboboxEmpty.razor",
      "content": "@namespace Lumeo\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n@* Only renders when the parent Combobox reports zero registered items — previously it rendered unconditionally\n   so users saw the empty state stacked under their visible items. *@\n@if (IsVisible)\n{\n    <div class=\"@CssClass\" @attributes=\"AdditionalAttributes\">\n        @if (ChildContent is not null)\n        {\n            @ChildContent\n        }\n        else\n        {\n            <span>@L[\"Combobox.NoResults\"]</span>\n        }\n    </div>\n}\n\n@code {\n    [CascadingParameter] public Combobox.ComboboxContext? Context { get; set; }\n    [Parameter] public RenderFragment? ChildContent { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private bool IsVisible => Context is null || (!Context.IsSearchLoading && Context.ItemCount == 0);\n\n    private string CssClass => $\"py-6 text-center text-sm text-muted-foreground {Class}\".Trim();\n}\n"
    },
    {
      "path": "UI/Combobox/ComboboxInput.razor",
      "content": "@namespace Lumeo\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n<div class=\"relative\">\n    <Blazicon Svg=\"Lucide.Search\" class=\"absolute start-2.5 top-2.5 h-4 w-4 text-muted-foreground\" />\n    <input type=\"text\"\n           role=\"combobox\"\n           aria-haspopup=\"listbox\"\n           aria-expanded=\"@Context.IsOpen.ToString().ToLower()\"\n           aria-controls=\"@Context.ContentId\"\n           aria-autocomplete=\"list\"\n           aria-activedescendant=\"@(Context.IsOpen ? Context.FocusedItemId : null)\"\n           class=\"@CssClass\"\n           placeholder=\"@(Placeholder ?? L[\"Combobox.SearchPlaceholder\"])\"\n           value=\"@Context.SearchText\"\n           @oninput=\"HandleInput\"\n           @onfocus=\"Open\"\n           @onkeydown=\"HandleKeyDown\"\n           @attributes=\"AdditionalAttributes\" />\n</div>\n\n@code {\n    [CascadingParameter] public Combobox.ComboboxContext Context { get; set; } = default!;\n    [Parameter] public string? Placeholder { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private string CssClass => $\"flex h-9 w-full rounded-md border {(Context.Invalid ? \"border-destructive\" : \"border-input\")} bg-transparent py-2 ps-8 pe-3 text-sm placeholder:text-muted-foreground focus-visible:outline-none {(Context.Invalid ? \"focus-visible:ring-[1.5px] focus-visible:ring-destructive/20\" : \"\")} {Class}\".Trim();\n\n    private async Task HandleInput(ChangeEventArgs e)\n    {\n        var text = e.Value?.ToString() ?? \"\";\n        await Context.OnSearch.InvokeAsync(text);\n        if (!Context.IsOpen)\n        {\n            await Context.SetOpen.InvokeAsync(true);\n        }\n    }\n\n    private async Task Open()\n    {\n        if (!Context.IsOpen)\n        {\n            await Context.SetOpen.InvokeAsync(true);\n        }\n    }\n\n    private async Task HandleKeyDown(KeyboardEventArgs e)\n    {\n        switch (e.Key)\n        {\n            case \"ArrowDown\":\n                if (!Context.IsOpen)\n                    await Context.SetOpen.InvokeAsync(true);\n                else\n                    await Context.SetFocusedIndex.InvokeAsync(Context.FocusedIndex + 1);\n                break;\n            case \"ArrowUp\":\n                if (Context.IsOpen)\n                    await Context.SetFocusedIndex.InvokeAsync(Context.FocusedIndex > 0 ? Context.FocusedIndex - 1 : 0);\n                break;\n            case \"Home\":\n                if (Context.IsOpen)\n                    await Context.SetFocusedIndex.InvokeAsync(0);\n                break;\n            case \"End\":\n                if (Context.IsOpen)\n                    await Context.SetFocusedIndex.InvokeAsync(int.MaxValue);\n                break;\n            case \"Enter\":\n                if (Context.IsOpen && Context.FocusedItemValue is not null)\n                    await Context.OnSelect.InvokeAsync(Context.FocusedItemValue);\n                break;\n            case \"Escape\":\n                await Context.SetOpen.InvokeAsync(false);\n                break;\n        }\n    }\n}\n"
    },
    {
      "path": "UI/Combobox/ComboboxItem.razor",
      "content": "@namespace Lumeo\n@implements IDisposable\n\n@if (Visible)\n{\n    <button type=\"button\" id=\"@_itemId\" role=\"option\" aria-selected=\"@IsSelected.ToString().ToLower()\"\n            class=\"@CssClass\" @onclick=\"Select\" @attributes=\"AdditionalAttributes\">\n        <span class=\"flex h-3.5 w-3.5 items-center justify-center\">\n            @if (IsSelected)\n            {\n                <Blazicon Svg=\"Lucide.Check\" class=\"h-3.5 w-3.5\" />\n            }\n        </span>\n        @ChildContent\n    </button>\n}\n\n@code {\n    [CascadingParameter] public Combobox.ComboboxContext Context { get; set; } = default!;\n    [Parameter] public RenderFragment? ChildContent { get; set; }\n    [Parameter, EditorRequired] public string Value { get; set; } = \"\";\n    [Parameter] public string? Class { get; set; }\n    /// <summary>Optional text used to match against the search input. Falls back to Value.</summary>\n    [Parameter] public string? SearchValue { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private readonly string _itemId = $\"combobox-item-{Guid.NewGuid():N}\";\n    private bool _registered;\n\n    private bool IsSelected => Context.Multiple\n        ? Context.Values?.Contains(Value) == true\n        : Context.Value == Value;\n\n    private bool IsFocused => Context.FocusedItemId == _itemId;\n\n    private bool Visible\n    {\n        get\n        {\n            if (string.IsNullOrEmpty(Context.SearchText)) return true;\n            var haystack = SearchValue ?? Value;\n            return haystack.Contains(Context.SearchText, StringComparison.OrdinalIgnoreCase);\n        }\n    }\n\n    private string CssClass => string.Join(\" \", new[] { \"relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground\", IsFocused ? \"bg-accent text-accent-foreground\" : null, Class }.Where(s => !string.IsNullOrWhiteSpace(s)));\n\n    protected override void OnParametersSet()\n    {\n        if (Visible && !_registered)\n        {\n            Context.RegisterItem(_itemId, Value);\n            _registered = true;\n        }\n        else if (!Visible && _registered)\n        {\n            Context.UnregisterItem(_itemId);\n            _registered = false;\n        }\n    }\n\n    public void Dispose()\n    {\n        if (_registered) Context.UnregisterItem(_itemId);\n    }\n\n    private async Task Select() => await Context.OnSelect.InvokeAsync(Value);\n}\n"
    }
  ],
  "slots": [
    {
      "name": "ChildContent",
      "owner": "Combobox",
      "type": "RenderFragment?",
      "description": null
    },
    {
      "name": "ItemTemplate",
      "owner": "Combobox",
      "type": "RenderFragment<object>?",
      "description": "Optional custom render fragment for an item's content. Receives the raw item object."
    },
    {
      "name": "ChildContent",
      "owner": "ComboboxContent",
      "type": "RenderFragment?",
      "description": null
    },
    {
      "name": "ChildContent",
      "owner": "ComboboxEmpty",
      "type": "RenderFragment?",
      "description": null
    },
    {
      "name": "ChildContent",
      "owner": "ComboboxItem",
      "type": "RenderFragment?",
      "description": null
    }
  ],
  "serviceDependencies": [
    {
      "service": "ComponentInteropService",
      "namespace": ""
    },
    {
      "service": "ILumeoLocalizer",
      "namespace": "Lumeo.Services.Localization"
    }
  ],
  "cascadingDependencies": [
    {
      "name": "FormField",
      "type": "FormField.FormFieldContext?",
      "required": false
    },
    {
      "name": "Context",
      "type": "Combobox.ComboboxContext",
      "required": true
    }
  ],
  "relatedComponents": [
    {
      "name": "ComboboxContent",
      "reason": "sub-component"
    },
    {
      "name": "ComboboxCreate",
      "reason": "sub-component"
    },
    {
      "name": "ComboboxEmpty",
      "reason": "sub-component"
    },
    {
      "name": "ComboboxInput",
      "reason": "sub-component"
    },
    {
      "name": "ComboboxItem",
      "reason": "sub-component"
    },
    {
      "name": "Spinner",
      "reason": "used internally"
    }
  ],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Tests/Components/Combobox/ComboboxDataBoundTests.cs",
    "tests/Lumeo.Tests/Components/Combobox/ComboboxTests.cs"
  ],
  "mdSummary": "# Combobox\n\nSearchable select with filtering, custom values, and grouping.\n\n**Package:** `Lumeo` · **Category:** Forms/Selection\n**Docs:** https://lumeo.nativ.sh/components/combobox\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| Combobox | FormField | `FormField.FormFieldContext?` | `` |  |\n| Combobox | ChildContent | `RenderFragment?` | `` |  |\n| Combobox | Value | `string?` | `` |  |\n| Combobox | ValueChanged | `EventCallback<string>` | `` |  |\n| Combobox | Open | `bool` | `` | Popover open state. Bind via @bind-Open when the Combobox lives inside a parent that re-renders heavily (e.g. an EditForm with active validation) — otherwise a parameter cascade can close the popover mid-click. In the bound (controlled) mode the parent owns state and the popover survives re-renders. When unbound, the component still functions but is vulnerable to the cascade-close race; binding is the recommended pattern. |\n| Combobox | OpenChanged | `EventCallback<bool>` | `` |  |\n| Combobox | IsOpen | `bool` | `` |  |\n| Combobox | IsOpenChanged | `EventCallback<bool>` | `` |  |\n| Combobox | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| Combobox | Multiple | `bool` | `` |  |\n| Combobox | Values | `HashSet<string>?` | `` |  |\n| Combobox | ValuesChanged | `EventCallback<HashSet<string>>` | `` |  |\n| Combobox | Creatable | `bool` | `` |  |\n| Combobox | OnCreate | `EventCallback<string>` | `` |  |\n| Combobox | OnSearchAsync | `EventCallback<string>` | `` |  |\n| Combobox | DebounceMs | `int` | `300` |  |\n| Combobox | IsSearchLoading | `bool` | `` |  |\n| Combobox | Required | `bool` | `` |  |\n| Combobox | Invalid | `bool` | `` |  |\n| Combobox | ErrorText | `string?` | `` |  |\n| Combobox | HelperText | `string?` | `` |  |\n| Combobox | Label | `string?` | `` |  |\n| Combobox | Name | `string?` | `` |  |\n| Combobox | Items | `IEnumerable<object>?` | `` | Optional data source. When set, ComboboxContent renders options from this list instead of ChildContent. |\n| Combobox | ItemValue | `Func<object, string>` | `it => it?.ToString() ?? \"\"` | Extracts the string value for each item. Defaults to ToString(). |\n| Combobox | ItemText | `Func<object, string>?` | `` | Extracts the display label for each item. Defaults to ItemValue when null. |\n| Combobox | ItemGroup | `Func<object, string?>?` | `` | Optional group key extractor. When set, options are grouped under sticky headers. |\n| Combobox | ItemDisabled | `Func<object, bool>?` | `` | Optional per-item disabled predicate. |\n| Combobox | ItemTemplate | `RenderFragment<object>?` | `` | Optional custom render fragment for an item's content. Receives the raw item object. |\n| Combobox | Virtualize | `bool` | `` | When true and Items is set, uses Blazor's Virtualize component for the option list. |\n| Combobox | ItemSize | `float` | `36f` | Estimated pixel height of each option row used by the Virtualize component. Default 36. |\n| ComboboxContent | Context | `Combobox.ComboboxContext` | `default!` |  |\n| ComboboxContent | ChildContent | `RenderFragment?` | `` |  |\n| ComboboxContent | EmptyText | `string?` | `` |  |\n| ComboboxContent | Class | `string?` | `` |  |\n| ComboboxContent | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| ComboboxCreate | Context | `Combobox.ComboboxContext` | `default!` |  |\n| ComboboxCreate | OnCreate | `EventCallback<string>` | `` |  |\n| ComboboxCreate | ShowCreate | `bool` | `true` |  |\n| ComboboxCreate | Class | `string?` | `` |  |\n| ComboboxCreate | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| ComboboxEmpty | Context | `Combobox.ComboboxContext?` | `` |  |\n| ComboboxEmpty | ChildContent | `RenderFragment?` | `` |  |\n| ComboboxEmpty | Class | `string?` | `` |  |\n| ComboboxEmpty | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| ComboboxInput | Context | `Combobox.ComboboxContext` | `default!` |  |\n| ComboboxInput | Placeholder | `string?` | `` |  |\n| ComboboxInput | Class | `string?` | `` |  |\n| ComboboxInput | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| ComboboxItem | Context | `Combobox.ComboboxContext` | `default!` |  |\n| ComboboxItem | ChildContent | `RenderFragment?` | `` |  |\n| ComboboxItem | Value | `string` | `\"\"` |  |\n| ComboboxItem | Class | `string?` | `` |  |\n| ComboboxItem | SearchValue | `string?` | `` | Optional text used to match against the search input. Falls back to Value. |\n| ComboboxItem | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- `ChildContent` (Combobox) — RenderFragment slot\n- `ItemTemplate` (Combobox) — Optional custom render fragment for an item's content. Receives the raw item object.\n- `ChildContent` (ComboboxContent) — RenderFragment slot\n- `ChildContent` (ComboboxEmpty) — RenderFragment slot\n- `ChildContent` (ComboboxItem) — RenderFragment slot\n\n## Events\n- `ValueChanged` (`EventCallback<string>`)\n- `OpenChanged` (`EventCallback<bool>`)\n- `IsOpenChanged` (`EventCallback<bool>`)\n- `ValuesChanged` (`EventCallback<HashSet<string>>`)\n- `OnCreate` (`EventCallback<string>`)\n- `OnSearchAsync` (`EventCallback<string>`)\n\n## Sub-components\n- `ComboboxContent`, `ComboboxCreate`, `ComboboxEmpty`, `ComboboxInput`, `ComboboxItem`\n\n## Cascading values it expects\n- `FormField` (`FormField.FormFieldContext?`)\n- `Context` (`Combobox.ComboboxContext`)\n\n## Services it injects\n- `ComponentInteropService` ()\n- `ILumeoLocalizer` (Lumeo.Services.Localization)\n\n## Keyboard\n—\n\n## Example\n```razor\n<Combobox />\n```\n\n## CSS variables\n`--color-accent`, `--color-accent-foreground`, `--color-border`, `--color-destructive`, `--color-input`, `--color-muted`, `--color-muted-foreground`, `--color-popover`, `--color-popover-foreground`, `--radius`\n\n## Files\n- `UI/Combobox/Combobox.razor`\n- `UI/Combobox/ComboboxContent.razor`\n- `UI/Combobox/ComboboxCreate.razor`\n- `UI/Combobox/ComboboxEmpty.razor`\n- `UI/Combobox/ComboboxInput.razor`\n- `UI/Combobox/ComboboxItem.razor`\n"
}