{
  "name": "Sortable",
  "category": "Drag & Drop",
  "subcategory": null,
  "description": "Drag-and-drop reorderable list.",
  "thumbnail": "/preview-cards/sortable.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/Sortable/SortableList.razor"
  ],
  "dependencies": [
    "list"
  ],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-border",
    "--color-card",
    "--color-foreground",
    "--color-muted",
    "--color-muted-foreground",
    "--color-primary",
    "--radius"
  ],
  "api": {
    "name": "Sortable",
    "category": "Drag & Drop",
    "subcategory": null,
    "description": "Drag-and-drop reorderable list.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/Sortable/SortableList.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IAsyncDisposable"
    ],
    "parameters": [
      {
        "name": "Items",
        "type": "List<TItem>",
        "default": "[]",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ItemsChanged",
        "type": "EventCallback<List<TItem>>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ItemTemplate",
        "type": "RenderFragment<TItem>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Handle",
        "type": "bool",
        "default": "true",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Disabled",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Group",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Key",
        "type": "Func<TItem, object>?",
        "default": null,
        "description": "Optional selector that returns a stable, unique key for each item. Providing this ensures Blazor's diffing engine tracks items by identity rather than index — critical for drag-reorder to map DOM state correctly. If omitted, the item reference itself is used as the key.",
        "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": "ItemsChanged",
        "type": "EventCallback<List<TItem>>",
        "description": null
      }
    ],
    "enums": [],
    "records": [],
    "cssVars": [
      "--color-border",
      "--color-card",
      "--color-foreground",
      "--color-muted",
      "--color-muted-foreground",
      "--color-primary",
      "--radius"
    ],
    "examples": [
      {
        "title": "Basic Sortable List",
        "code": "<SortableList TItem=\"string\" Items=\"@_tasks\" ItemsChanged=\"@(v => _tasks = v)\">\n    <ItemTemplate Context=\"item\">\n        <Lumeo.Text Class=\"text-sm\">@item</Lumeo.Text>\n    </ItemTemplate>\n</SortableList>"
      },
      {
        "title": "No Handle (drag entire card)",
        "code": "<SortableList TItem=\"string\" Items=\"@_tasks\" ItemsChanged=\"@(v => _tasks = v)\" Handle=\"false\">\n    <ItemTemplate Context=\"item\">\n        <Lumeo.Text Class=\"text-sm\">@item</Lumeo.Text>\n    </ItemTemplate>\n</SortableList>"
      },
      {
        "title": "Rich Item Template",
        "code": "<SortableList TItem=\"TaskItem\" Items=\"@_tasks\" ItemsChanged=\"@(v => _tasks = v)\">\n    <ItemTemplate Context=\"task\">\n        <Flex Class=\"items-center gap-3 w-full\">\n            <DynamicIcon Name=\"@task.Icon\" Class=\"h-4 w-4 text-primary\" />\n            <Stack Gap=\"0\" Class=\"flex-1\">\n                <Lumeo.Text Class=\"text-sm font-medium\">@task.Title</Lumeo.Text>\n                <Lumeo.Text Class=\"text-xs text-muted-foreground\">@task.Assignee</Lumeo.Text>\n            </Stack>\n            <Badge Variant=\"Badge.BadgeVariant.Secondary\">@task.Priority</Badge>\n        </Flex>\n    </ItemTemplate>\n</SortableList>"
      },
      {
        "title": "Disabled",
        "code": "<SortableList TItem=\"string\" Items=\"@_tasks\" Disabled=\"true\">\n    <ItemTemplate Context=\"item\">\n        <Lumeo.Text Class=\"text-sm\">@item</Lumeo.Text>\n    </ItemTemplate>\n</SortableList>"
      }
    ],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "sortable",
  "docsUrl": "https://lumeo.nativ.sh/components/sortable",
  "sourceUrl": [
    {
      "file": "UI/Sortable/SortableList.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Sortable/SortableList.razor"
    }
  ],
  "source": [
    {
      "path": "UI/Sortable/SortableList.razor",
      "content": "@namespace Lumeo\n@typeparam TItem\n@implements IAsyncDisposable\n@inject Lumeo.Services.ComponentInteropService Interop\n\n<div id=\"@_containerId\" class=\"@CssClass\" @attributes=\"AdditionalAttributes\">\n    @foreach (var (item, index) in _items.Select((x, i) => (x, i)))\n    {\n        <div @key=\"@(Key is not null ? Key(item) : (object?)item)\" class=\"@GetItemClass(index)\"\n             data-sortable-item\n             data-sortable-index=\"@index\"\n             draggable=\"@(!Disabled ? \"true\" : \"false\")\"\n             @ondragstart=\"() => OnDragStart(index)\"\n             @ondragover:preventDefault=\"true\"\n             @ondragover=\"() => OnDragOver(index)\"\n             @ondragleave=\"OnDragLeave\"\n             @ondrop=\"() => OnDrop(index)\"\n             @ondragend=\"OnDragEnd\">\n            @if (Handle && !Disabled)\n            {\n                <div class=\"flex-shrink-0 cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground mr-2 flex items-center justify-center h-11 w-11 -my-2 sm:h-6 sm:w-6 sm:my-0\"\n                     role=\"button\" aria-label=\"Drag handle\" tabindex=\"0\">\n                    <Blazicon Svg=\"Lucide.GripVertical\" class=\"h-4 w-4\" />\n                </div>\n            }\n            <div class=\"flex-1 min-w-0\">\n                @if (ItemTemplate is not null)\n                {\n                    @ItemTemplate(item)\n                }\n            </div>\n        </div>\n    }\n</div>\n\n@code {\n    [Parameter] public List<TItem> Items { get; set; } = [];\n    [Parameter] public EventCallback<List<TItem>> ItemsChanged { get; set; }\n    [Parameter] public RenderFragment<TItem>? ItemTemplate { get; set; }\n    [Parameter] public bool Handle { get; set; } = true;\n    [Parameter] public bool Disabled { get; set; }\n    [Parameter] public string? Group { get; set; }\n\n    /// <summary>\n    /// Optional selector that returns a stable, unique key for each item.\n    /// Providing this ensures Blazor's diffing engine tracks items by identity\n    /// rather than index — critical for drag-reorder to map DOM state correctly.\n    /// If omitted, the item reference itself is used as the key.\n    /// </summary>\n    [Parameter] public Func<TItem, object>? Key { get; set; }\n\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private readonly string _containerId = $\"sortable-{Guid.NewGuid():N}\";\n    private List<TItem> _items = [];\n    private int _dragFromIndex = -1;\n    private int _dragOverIndex = -1;\n    private bool _touchRegistered;\n\n    protected override void OnParametersSet()\n    {\n        _items = Items.ToList();\n    }\n\n    // rc.44: HTML5 Drag-and-Drop doesn't fire on touch devices, so we register\n    // a parallel touch path that round-trips source/target indices via the\n    // interop service. The handler converges with the desktop OnDrop logic.\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (firstRender && !Disabled && !_touchRegistered)\n        {\n            await Interop.RegisterSortableTouch(_containerId, HandleTouchDrop);\n            _touchRegistered = true;\n        }\n    }\n\n    private async Task HandleTouchDrop(int source, int target)\n    {\n        if (Disabled) return;\n        await InvokeAsync(async () =>\n        {\n            if (source < 0 || target < 0 || source >= _items.Count || target >= _items.Count || source == target)\n                return;\n\n            var item = _items[source];\n            _items.RemoveAt(source);\n            _items.Insert(target, item);\n\n            _dragFromIndex = -1;\n            _dragOverIndex = -1;\n\n            StateHasChanged();\n            await ItemsChanged.InvokeAsync(_items.ToList());\n        });\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        if (_touchRegistered)\n        {\n            await Interop.UnregisterSortableTouch(_containerId);\n            _touchRegistered = false;\n        }\n    }\n\n    private string CssClass => $\"flex flex-col gap-1 {Class}\".Trim();\n\n    private string GetItemClass(int index)\n    {\n        var classes = \"flex items-center rounded-md border border-border/40 bg-card px-3 py-2 transition-all touch-none select-none\";\n        if (index == _dragFromIndex)\n            classes += \" opacity-50\";\n        if (index == _dragOverIndex && index != _dragFromIndex)\n            classes += \" border-primary border-2\";\n        if (!Handle && !Disabled)\n            classes += \" cursor-grab active:cursor-grabbing\";\n        return classes;\n    }\n\n    private void OnDragStart(int index)\n    {\n        _dragFromIndex = index;\n    }\n\n    private void OnDragOver(int index)\n    {\n        _dragOverIndex = index;\n    }\n\n    private void OnDragLeave()\n    {\n        _dragOverIndex = -1;\n    }\n\n    private async Task OnDrop(int toIndex)\n    {\n        if (_dragFromIndex < 0 || _dragFromIndex == toIndex)\n        {\n            _dragFromIndex = -1;\n            _dragOverIndex = -1;\n            return;\n        }\n\n        var item = _items[_dragFromIndex];\n        _items.RemoveAt(_dragFromIndex);\n        _items.Insert(toIndex, item);\n\n        _dragFromIndex = -1;\n        _dragOverIndex = -1;\n\n        await ItemsChanged.InvokeAsync(_items.ToList());\n    }\n\n    private void OnDragEnd()\n    {\n        _dragFromIndex = -1;\n        _dragOverIndex = -1;\n    }\n}\n"
    }
  ],
  "slots": [
    {
      "name": "ItemTemplate",
      "owner": "Sortable",
      "type": "RenderFragment<TItem>?",
      "description": null
    }
  ],
  "serviceDependencies": [
    {
      "service": "ComponentInteropService",
      "namespace": "Lumeo.Services"
    }
  ],
  "cascadingDependencies": [],
  "relatedComponents": [
    {
      "name": "SortableList",
      "reason": "sub-component"
    },
    {
      "name": "List",
      "reason": "used internally"
    }
  ],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Docs.Tests/Helpers/DocsTestContext.cs",
    "tests/Lumeo.Tests/Components/DataGrid/DataGridAutoSaveStaleTests.cs",
    "tests/Lumeo.Tests/Components/DataGrid/DataGridExpandableTests.cs",
    "tests/Lumeo.Tests/Components/DataGrid/DataGridKeyboardNavTests.cs",
    "tests/Lumeo.Tests/Components/DataGrid/DataGridLayoutJsonTests.cs",
    "tests/Lumeo.Tests/Components/DataGrid/DataGridTests.cs",
    "tests/Lumeo.Tests/Components/DataGrid/DataGridToolbarTogglesTests.cs",
    "tests/Lumeo.Tests/Components/DataGrid/DataGridVirtualizedServerModeTests.cs",
    "tests/Lumeo.Tests/Components/Sortable/SortableListTests.cs",
    "tests/Lumeo.Tests/Contract/ComponentContractTests.cs"
  ],
  "mdSummary": "# Sortable\n\nDrag-and-drop reorderable list.\n\n**Package:** `Lumeo` · **Category:** Drag & Drop\n**Docs:** https://lumeo.nativ.sh/components/sortable\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| Sortable | Items | `List<TItem>` | `[]` |  |\n| Sortable | ItemsChanged | `EventCallback<List<TItem>>` | `` |  |\n| Sortable | ItemTemplate | `RenderFragment<TItem>?` | `` |  |\n| Sortable | Handle | `bool` | `true` |  |\n| Sortable | Disabled | `bool` | `` |  |\n| Sortable | Group | `string?` | `` |  |\n| Sortable | Key | `Func<TItem, object>?` | `` | Optional selector that returns a stable, unique key for each item. Providing this ensures Blazor's diffing engine tracks items by identity rather than index — critical for drag-reorder to map DOM state correctly. If omitted, the item reference itself is used as the key. |\n| Sortable | Class | `string?` | `` |  |\n| Sortable | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- `ItemTemplate` (Sortable) — RenderFragment slot\n\n## Events\n- `ItemsChanged` (`EventCallback<List<TItem>>`)\n\n## Sub-components\n- `SortableList`\n\n## Cascading values it expects\n- (none)\n\n## Services it injects\n- `ComponentInteropService` (Lumeo.Services)\n\n## Keyboard\n—\n\n## Example\n```razor\n<Sortable />\n```\n\n## CSS variables\n`--color-border`, `--color-card`, `--color-foreground`, `--color-muted`, `--color-muted-foreground`, `--color-primary`, `--radius`\n\n## Files\n- `UI/Sortable/SortableList.razor`\n"
}