{
  "name": "PickList",
  "category": "Dashboard",
  "subcategory": null,
  "description": "Two-column shuttle picker — move items between lists.",
  "thumbnail": "/preview-cards/pick-list.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/PickList/PickList.razor"
  ],
  "dependencies": [],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-accent",
    "--color-accent-foreground",
    "--color-background",
    "--color-border",
    "--color-card",
    "--color-input",
    "--color-muted",
    "--color-muted-foreground",
    "--color-ring",
    "--radius"
  ],
  "api": {
    "name": "PickList",
    "category": "Dashboard",
    "subcategory": null,
    "description": "Two-column shuttle picker — move items between lists.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/PickList/PickList.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [],
    "parameters": [
      {
        "name": "Items",
        "type": "IEnumerable<TItem>",
        "default": "[]",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "SelectedItems",
        "type": "IEnumerable<TItem>",
        "default": "[]",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "SelectedItemsChanged",
        "type": "EventCallback<IEnumerable<TItem>>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ItemTemplate",
        "type": "RenderFragment<TItem>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "SourceTitle",
        "type": "string",
        "default": "\"Available\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "TargetTitle",
        "type": "string",
        "default": "\"Selected\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ShowSearch",
        "type": "bool",
        "default": "true",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Height",
        "type": "string",
        "default": "\"320px\"",
        "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": "SelectedItemsChanged",
        "type": "EventCallback<IEnumerable<TItem>>",
        "description": null
      }
    ],
    "enums": [],
    "records": [],
    "cssVars": [
      "--color-accent",
      "--color-accent-foreground",
      "--color-background",
      "--color-border",
      "--color-card",
      "--color-input",
      "--color-muted",
      "--color-muted-foreground",
      "--color-ring",
      "--radius"
    ],
    "examples": [
      {
        "title": "Strings",
        "code": "<PickList TItem=\"string\"\n          Items=\"_allDepartments\"\n          SelectedItems=\"_pickedDepartments\"\n          SelectedItemsChanged=\"OnDepartmentsChanged\"\n          SourceTitle=\"All Departments\"\n          TargetTitle=\"Assigned\" />"
      },
      {
        "title": "Typed Records with Item Template",
        "code": "<PickList TItem=\"UserInfo\"\n          Items=\"_allUsers\"\n          SelectedItems=\"_pickedUsers\"\n          SelectedItemsChanged=\"OnUsersChanged\"\n          SourceTitle=\"Users\"\n          TargetTitle=\"Team Members\">\n    <ItemTemplate>\n        <div class=\"flex flex-col\">\n            <span class=\"text-sm font-medium\">@context.Name</span>\n            <span class=\"text-xs text-muted-foreground\">@context.Email</span>\n        </div>\n    </ItemTemplate>\n</PickList>"
      }
    ],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "pick-list",
  "docsUrl": "https://lumeo.nativ.sh/components/pick-list",
  "sourceUrl": [
    {
      "file": "UI/PickList/PickList.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/PickList/PickList.razor"
    }
  ],
  "source": [
    {
      "path": "UI/PickList/PickList.razor",
      "content": "@namespace Lumeo\n@typeparam TItem\n\n<div class=\"@CssClass\"\n     style=\"display:grid;grid-template-columns:1fr auto 1fr;gap:0.75rem;align-items:stretch;\"\n     @attributes=\"AdditionalAttributes\">\n\n    @* Source panel *@\n    <div class=\"flex flex-col border border-border/40 rounded-lg overflow-hidden bg-card\" style=\"min-width:0;\">\n        <div class=\"px-3 py-2 border-b border-border/40 bg-muted/30 flex items-center justify-between\">\n            <span class=\"text-sm font-medium\">@SourceTitle</span>\n            <span class=\"text-xs text-muted-foreground\">@_sourceSelected.Count / @_sourceItems.Count</span>\n        </div>\n        @if (ShowSearch)\n        {\n            <div class=\"px-2 py-1.5 border-b border-border/40\">\n                <input type=\"text\" placeholder=\"Search…\"\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=\"@_sourceSearch\"\n                       @oninput=\"OnSourceSearch\" />\n            </div>\n        }\n        <div class=\"overflow-y-auto p-1\" style=\"height:@Height;\">\n            @if (FilteredSourceItems.Count > 0)\n            {\n                @foreach (var item in FilteredSourceItems)\n                {\n                    var selected = _sourceSelected.Contains(item);\n                    <button @key=\"(object?)item\" type=\"button\"\n                            class=\"@RowClass(selected)\"\n                            @onclick=\"() => ToggleSource(item)\">\n                        @if (ItemTemplate != null)\n                        {\n                            @ItemTemplate(item)\n                        }\n                        else\n                        {\n                            <span class=\"truncate\">@(item?.ToString() ?? \"\")</span>\n                        }\n                    </button>\n                }\n            }\n            else\n            {\n                <div class=\"text-center text-xs text-muted-foreground py-6\">No items</div>\n            }\n        </div>\n    </div>\n\n    @* Middle buttons *@\n    <div class=\"flex flex-col items-center justify-center gap-1.5\">\n        <button type=\"button\" class=\"@ButtonClass\" @onclick=\"MoveAllToTarget\" disabled=\"@(_sourceItems.Count == 0)\" title=\"Move all\">\n            <Blazicon Svg=\"Lucide.ChevronsRight\" class=\"h-4 w-4\" />\n        </button>\n        <button type=\"button\" class=\"@ButtonClass\" @onclick=\"MoveSelectedToTarget\" disabled=\"@(_sourceSelected.Count == 0)\" title=\"Move selected\">\n            <Blazicon Svg=\"Lucide.ChevronRight\" class=\"h-4 w-4\" />\n        </button>\n        <button type=\"button\" class=\"@ButtonClass\" @onclick=\"MoveSelectedToSource\" disabled=\"@(_targetSelected.Count == 0)\" title=\"Move back selected\">\n            <Blazicon Svg=\"Lucide.ChevronLeft\" class=\"h-4 w-4\" />\n        </button>\n        <button type=\"button\" class=\"@ButtonClass\" @onclick=\"MoveAllToSource\" disabled=\"@(_targetItems.Count == 0)\" title=\"Move back all\">\n            <Blazicon Svg=\"Lucide.ChevronsLeft\" class=\"h-4 w-4\" />\n        </button>\n    </div>\n\n    @* Target panel *@\n    <div class=\"flex flex-col border border-border/40 rounded-lg overflow-hidden bg-card\" style=\"min-width:0;\">\n        <div class=\"px-3 py-2 border-b border-border/40 bg-muted/30 flex items-center justify-between\">\n            <span class=\"text-sm font-medium\">@TargetTitle</span>\n            <span class=\"text-xs text-muted-foreground\">@_targetSelected.Count / @_targetItems.Count</span>\n        </div>\n        @if (ShowSearch)\n        {\n            <div class=\"px-2 py-1.5 border-b border-border/40\">\n                <input type=\"text\" placeholder=\"Search…\"\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=\"@_targetSearch\"\n                       @oninput=\"OnTargetSearch\" />\n            </div>\n        }\n        <div class=\"overflow-y-auto p-1\" style=\"height:@Height;\">\n            @if (FilteredTargetItems.Count > 0)\n            {\n                @foreach (var item in FilteredTargetItems)\n                {\n                    var selected = _targetSelected.Contains(item);\n                    <button @key=\"(object?)item\" type=\"button\"\n                            class=\"@RowClass(selected)\"\n                            @onclick=\"() => ToggleTarget(item)\">\n                        @if (ItemTemplate != null)\n                        {\n                            @ItemTemplate(item)\n                        }\n                        else\n                        {\n                            <span class=\"truncate\">@(item?.ToString() ?? \"\")</span>\n                        }\n                    </button>\n                }\n            }\n            else\n            {\n                <div class=\"text-center text-xs text-muted-foreground py-6\">No items</div>\n            }\n        </div>\n    </div>\n</div>\n\n@code {\n    [Parameter] public IEnumerable<TItem> Items { get; set; } = [];\n    [Parameter] public IEnumerable<TItem> SelectedItems { get; set; } = [];\n    [Parameter] public EventCallback<IEnumerable<TItem>> SelectedItemsChanged { get; set; }\n    [Parameter] public RenderFragment<TItem>? ItemTemplate { get; set; }\n    [Parameter] public string SourceTitle { get; set; } = \"Available\";\n    [Parameter] public string TargetTitle { get; set; } = \"Selected\";\n    [Parameter] public bool ShowSearch { get; set; } = true;\n    [Parameter] public string Height { get; set; } = \"320px\";\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private List<TItem> _sourceItems = new();\n    private List<TItem> _targetItems = new();\n    private readonly HashSet<TItem> _sourceSelected = new();\n    private readonly HashSet<TItem> _targetSelected = new();\n    private string _sourceSearch = \"\";\n    private string _targetSearch = \"\";\n\n    private string CssClass => $\"w-full {Class}\".Trim();\n\n    private const string ButtonClass = \"inline-flex items-center justify-center h-8 w-8 rounded-md border border-border/60 bg-background hover:bg-accent hover:text-accent-foreground transition-colors disabled:opacity-40 disabled:cursor-not-allowed\";\n\n    private string RowClass(bool selected) =>\n        selected\n            ? \"w-full text-left flex items-center gap-2 px-2 py-1.5 rounded-md text-sm bg-accent text-accent-foreground transition-colors\"\n            : \"w-full text-left flex items-center gap-2 px-2 py-1.5 rounded-md text-sm hover:bg-accent/50 transition-colors\";\n\n    protected override void OnParametersSet()\n    {\n        var all = (Items ?? []).ToList();\n        var sel = (SelectedItems ?? []).ToList();\n        var selSet = new HashSet<TItem>(sel);\n\n        _targetItems = sel;\n        _sourceItems = all.Where(i => !selSet.Contains(i)).ToList();\n\n        // Clean out selections that are no longer in their side\n        _sourceSelected.RemoveWhere(i => !_sourceItems.Contains(i));\n        _targetSelected.RemoveWhere(i => !_targetItems.Contains(i));\n    }\n\n    private List<TItem> FilteredSourceItems =>\n        string.IsNullOrWhiteSpace(_sourceSearch)\n            ? _sourceItems\n            : _sourceItems.Where(i => (i?.ToString() ?? \"\").Contains(_sourceSearch, StringComparison.OrdinalIgnoreCase)).ToList();\n\n    private List<TItem> FilteredTargetItems =>\n        string.IsNullOrWhiteSpace(_targetSearch)\n            ? _targetItems\n            : _targetItems.Where(i => (i?.ToString() ?? \"\").Contains(_targetSearch, StringComparison.OrdinalIgnoreCase)).ToList();\n\n    private void OnSourceSearch(ChangeEventArgs e) => _sourceSearch = e.Value?.ToString() ?? \"\";\n    private void OnTargetSearch(ChangeEventArgs e) => _targetSearch = e.Value?.ToString() ?? \"\";\n\n    private void ToggleSource(TItem item)\n    {\n        if (!_sourceSelected.Add(item)) _sourceSelected.Remove(item);\n    }\n\n    private void ToggleTarget(TItem item)\n    {\n        if (!_targetSelected.Add(item)) _targetSelected.Remove(item);\n    }\n\n    private Task MoveSelectedToTarget() => MoveToTarget(_sourceSelected.ToList());\n    private Task MoveAllToTarget() => MoveToTarget(_sourceItems.ToList());\n    private Task MoveSelectedToSource() => MoveToSource(_targetSelected.ToList());\n    private Task MoveAllToSource() => MoveToSource(_targetItems.ToList());\n\n    private async Task MoveToTarget(List<TItem> toMove)\n    {\n        if (toMove.Count == 0) return;\n        foreach (var i in toMove)\n        {\n            _sourceItems.Remove(i);\n            if (!_targetItems.Contains(i)) _targetItems.Add(i);\n        }\n        _sourceSelected.Clear();\n        await SelectedItemsChanged.InvokeAsync(_targetItems.ToList());\n    }\n\n    private async Task MoveToSource(List<TItem> toMove)\n    {\n        if (toMove.Count == 0) return;\n        foreach (var i in toMove)\n        {\n            _targetItems.Remove(i);\n            if (!_sourceItems.Contains(i)) _sourceItems.Add(i);\n        }\n        _targetSelected.Clear();\n        await SelectedItemsChanged.InvokeAsync(_targetItems.ToList());\n    }\n}\n"
    }
  ],
  "slots": [
    {
      "name": "ItemTemplate",
      "owner": "PickList",
      "type": "RenderFragment<TItem>?",
      "description": null
    }
  ],
  "serviceDependencies": [],
  "cascadingDependencies": [],
  "relatedComponents": [],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Tests/Components/PickList/PickListTests.cs",
    "tests/Lumeo.Tests/Contract/ComponentContractTests.cs"
  ],
  "mdSummary": "# PickList\n\nTwo-column shuttle picker — move items between lists.\n\n**Package:** `Lumeo` · **Category:** Dashboard\n**Docs:** https://lumeo.nativ.sh/components/pick-list\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| PickList | Items | `IEnumerable<TItem>` | `[]` |  |\n| PickList | SelectedItems | `IEnumerable<TItem>` | `[]` |  |\n| PickList | SelectedItemsChanged | `EventCallback<IEnumerable<TItem>>` | `` |  |\n| PickList | ItemTemplate | `RenderFragment<TItem>?` | `` |  |\n| PickList | SourceTitle | `string` | `\"Available\"` |  |\n| PickList | TargetTitle | `string` | `\"Selected\"` |  |\n| PickList | ShowSearch | `bool` | `true` |  |\n| PickList | Height | `string` | `\"320px\"` |  |\n| PickList | Class | `string?` | `` |  |\n| PickList | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- `ItemTemplate` (PickList) — RenderFragment slot\n\n## Events\n- `SelectedItemsChanged` (`EventCallback<IEnumerable<TItem>>`)\n\n## Sub-components\n- (none)\n\n## Cascading values it expects\n- (none)\n\n## Services it injects\n- (none)\n\n## Keyboard\n—\n\n## Example\n```razor\n<PickList />\n```\n\n## CSS variables\n`--color-accent`, `--color-accent-foreground`, `--color-background`, `--color-border`, `--color-card`, `--color-input`, `--color-muted`, `--color-muted-foreground`, `--color-ring`, `--radius`\n\n## Files\n- `UI/PickList/PickList.razor`\n"
}