{
  "name": "FileManager",
  "category": "Data Display",
  "subcategory": null,
  "description": "Headless file and folder explorer — folder tree, breadcrumb path, list/grid views, lazy loading, inline rename, context-menu operations.",
  "thumbnail": "/preview-cards/file-manager.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/FileManager/FileManager.razor",
    "UI/FileManager/FileManagerModel.cs"
  ],
  "dependencies": [
    "list"
  ],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-accent",
    "--color-accent-foreground",
    "--color-background",
    "--color-border",
    "--color-card",
    "--color-destructive",
    "--color-foreground",
    "--color-muted",
    "--color-muted-foreground",
    "--color-popover",
    "--color-popover-foreground",
    "--color-ring",
    "--radius"
  ],
  "api": {
    "name": "FileManager",
    "category": "Data Display",
    "subcategory": null,
    "description": "Headless file and folder explorer — folder tree, breadcrumb path, list/grid views, lazy loading, inline rename, context-menu operations.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/FileManager/FileManager.razor",
      "UI/FileManager/FileManagerModel.cs"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [],
    "parameters": [
      {
        "name": "Root",
        "type": "IReadOnlyList<FileSystemNode>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "CurrentPath",
        "type": "string?",
        "default": null,
        "description": "The ID of the currently open folder. Null means the root level. Uses Id as the path identifier.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "CurrentPathChanged",
        "type": "EventCallback<string?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "LoadChildren",
        "type": "Func<FileSystemNode, Task<List<FileSystemNode>>>?",
        "default": null,
        "description": "Called the first time a folder is opened if its Children is null. Return the loaded children.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ViewMode",
        "type": "FileManagerViewMode",
        "default": "FileManagerViewMode.List",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ViewModeChanged",
        "type": "EventCallback<FileManagerViewMode>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "SelectedNode",
        "type": "FileSystemNode?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "SelectedNodeChanged",
        "type": "EventCallback<FileSystemNode?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnFileOpen",
        "type": "EventCallback<FileSystemNode>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnRename",
        "type": "EventCallback<(FileSystemNode Node, string NewName)>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnDelete",
        "type": "EventCallback<IReadOnlyList<FileSystemNode>>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnNewFolder",
        "type": "EventCallback<FileSystemNode>",
        "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": "CurrentPathChanged",
        "type": "EventCallback<string?>",
        "description": null
      },
      {
        "name": "ViewModeChanged",
        "type": "EventCallback<FileManagerViewMode>",
        "description": null
      },
      {
        "name": "SelectedNodeChanged",
        "type": "EventCallback<FileSystemNode?>",
        "description": null
      },
      {
        "name": "OnFileOpen",
        "type": "EventCallback<FileSystemNode>",
        "description": null
      },
      {
        "name": "OnRename",
        "type": "EventCallback<(FileSystemNode Node, string NewName)>",
        "description": null
      },
      {
        "name": "OnDelete",
        "type": "EventCallback<IReadOnlyList<FileSystemNode>>",
        "description": null
      },
      {
        "name": "OnNewFolder",
        "type": "EventCallback<FileSystemNode>",
        "description": null
      }
    ],
    "enums": [
      {
        "name": "FileManagerViewMode",
        "values": [
          "List",
          "Grid"
        ],
        "description": null
      }
    ],
    "records": [],
    "cssVars": [
      "--color-accent",
      "--color-accent-foreground",
      "--color-background",
      "--color-border",
      "--color-card",
      "--color-destructive",
      "--color-foreground",
      "--color-muted",
      "--color-muted-foreground",
      "--color-popover",
      "--color-popover-foreground",
      "--color-ring",
      "--radius"
    ],
    "examples": [],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "file-manager",
  "docsUrl": "https://lumeo.nativ.sh/components/file-manager",
  "sourceUrl": [
    {
      "file": "UI/FileManager/FileManager.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/FileManager/FileManager.razor"
    },
    {
      "file": "UI/FileManager/FileManagerModel.cs",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/FileManager/FileManagerModel.cs"
    }
  ],
  "source": [
    {
      "path": "UI/FileManager/FileManager.razor",
      "content": "@namespace Lumeo\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n<div class=\"@CssClass\" @attributes=\"AdditionalAttributes\">\n    @* ── Toolbar ─────────────────────────────────────────────────────────── *@\n    <div class=\"flex items-center gap-2 px-3 py-2 border-b border-border/40 bg-card shrink-0\">\n        @* Up button *@\n        <button type=\"button\"\n                class=\"@UpButtonClass\"\n                @onclick=\"NavigateUp\"\n                disabled=\"@(_pathStack.Count == 0)\"\n                title=\"Up one level\"\n                aria-label=\"Navigate up\">\n            <Blazicon Svg=\"Lucide.ArrowUp\" class=\"h-4 w-4\" />\n        </button>\n\n        @* Breadcrumb path *@\n        <nav aria-label=\"file manager path\" class=\"flex-1 min-w-0\">\n            <ol class=\"flex items-center flex-wrap gap-0.5 text-sm\">\n                @* Root segment *@\n                <li>\n                    <button type=\"button\"\n                            class=\"@BreadcrumbLinkClass\"\n                            @onclick=\"() => NavigateToStackDepth(-1)\"\n                            aria-label=\"Root\">\n                        <Blazicon Svg=\"Lucide.HardDrive\" class=\"h-3.5 w-3.5\" />\n                    </button>\n                </li>\n\n                @for (int i = 0; i < _pathStack.Count; i++)\n                {\n                    var depth = i;\n                    var seg = _pathStack[i];\n                    var isLast = i == _pathStack.Count - 1;\n\n                    <li class=\"flex items-center gap-0.5\">\n                        <span class=\"text-muted-foreground\">/</span>\n                        @if (isLast)\n                        {\n                            <span class=\"font-medium text-foreground truncate max-w-[120px]\">@seg.Name</span>\n                        }\n                        else\n                        {\n                            <button type=\"button\"\n                                    class=\"@BreadcrumbLinkClass truncate max-w-[120px]\"\n                                    @onclick=\"() => NavigateToStackDepth(depth)\">\n                                @seg.Name\n                            </button>\n                        }\n                    </li>\n                }\n            </ol>\n        </nav>\n\n        @* Toolbar actions *@\n        <div class=\"flex items-center gap-1 shrink-0\">\n            @if (OnNewFolder.HasDelegate)\n            {\n                <button type=\"button\"\n                        class=\"@ToolbarButtonClass\"\n                        @onclick=\"HandleNewFolder\"\n                        title=\"New folder\"\n                        aria-label=\"New folder\">\n                    <Blazicon Svg=\"Lucide.FolderPlus\" class=\"h-4 w-4\" />\n                </button>\n            }\n            @if (OnDelete.HasDelegate && SelectedNode is not null)\n            {\n                <button type=\"button\"\n                        class=\"@ToolbarButtonClass\"\n                        @onclick=\"HandleDeleteSelected\"\n                        title=\"Delete selected\"\n                        aria-label=\"Delete selected\">\n                    <Blazicon Svg=\"Lucide.Trash2\" class=\"h-4 w-4\" />\n                </button>\n            }\n            <div class=\"w-px h-4 bg-border/60 mx-0.5\"></div>\n            @* View mode toggle *@\n            <button type=\"button\"\n                    class=\"@(ViewMode == FileManagerViewMode.List ? ToolbarButtonActiveClass : ToolbarButtonClass)\"\n                    @onclick=\"() => SetViewMode(FileManagerViewMode.List)\"\n                    title=\"List view\"\n                    aria-label=\"Switch to list view\"\n                    aria-pressed=\"@(ViewMode == FileManagerViewMode.List)\">\n                <Blazicon Svg=\"Lucide.List\" class=\"h-4 w-4\" />\n            </button>\n            <button type=\"button\"\n                    class=\"@(ViewMode == FileManagerViewMode.Grid ? ToolbarButtonActiveClass : ToolbarButtonClass)\"\n                    @onclick=\"() => SetViewMode(FileManagerViewMode.Grid)\"\n                    title=\"Grid view\"\n                    aria-label=\"Switch to grid view\"\n                    aria-pressed=\"@(ViewMode == FileManagerViewMode.Grid)\">\n                <Blazicon Svg=\"Lucide.LayoutGrid\" class=\"h-4 w-4\" />\n            </button>\n        </div>\n    </div>\n\n    @* ── Body ─────────────────────────────────────────────────────────────── *@\n    <div class=\"flex flex-1 min-h-0 overflow-hidden\">\n\n        @* ── Left: folder tree ──────────────────────────────────────────── *@\n        <div class=\"w-52 shrink-0 border-r border-border/40 overflow-y-auto bg-card p-2\" role=\"tree\" aria-label=\"Folder tree\">\n            @foreach (var node in Root ?? [])\n            {\n                @RenderTreeNode(node, 0)\n            }\n        </div>\n\n        @* ── Right: file list ───────────────────────────────────────────── *@\n        <div class=\"flex-1 overflow-auto bg-background p-2\">\n            @{\n                var items = GetCurrentFolderContents();\n            }\n\n            @if (_loadingFolder)\n            {\n                <div class=\"flex items-center justify-center h-32 text-muted-foreground text-sm\">\n                    <Blazicon Svg=\"Lucide.Loader\" class=\"h-5 w-5 animate-spin mr-2\" />\n                    Loading...\n                </div>\n            }\n            else if (items.Count == 0)\n            {\n                <div class=\"flex flex-col items-center justify-center h-32 text-center\">\n                    <Blazicon Svg=\"Lucide.FolderOpen\" class=\"h-10 w-10 text-muted-foreground mb-3\" />\n                    <p class=\"text-sm font-medium text-foreground\">@L[\"FileManager.EmptyTitle\"]</p>\n                    <p class=\"text-xs text-muted-foreground mt-1\">@L[\"FileManager.EmptyState\"]</p>\n                </div>\n            }\n            else if (ViewMode == FileManagerViewMode.List)\n            {\n                @RenderListView(items)\n            }\n            else\n            {\n                @RenderGridView(items)\n            }\n        </div>\n    </div>\n</div>\n\n@code {\n    // ──────────────────────────────────────────────────────────────────────────\n    // Parameters\n    // ──────────────────────────────────────────────────────────────────────────\n\n    [Parameter] public IReadOnlyList<FileSystemNode>? Root { get; set; }\n\n    /// <summary>\n    /// The ID of the currently open folder. Null means the root level.\n    /// Uses <see cref=\"FileSystemNode.Id\"/> as the path identifier.\n    /// </summary>\n    [Parameter] public string? CurrentPath { get; set; }\n    [Parameter] public EventCallback<string?> CurrentPathChanged { get; set; }\n\n    /// <summary>Called the first time a folder is opened if its Children is null. Return the loaded children.</summary>\n    [Parameter] public Func<FileSystemNode, Task<List<FileSystemNode>>>? LoadChildren { get; set; }\n\n    [Parameter] public FileManagerViewMode ViewMode { get; set; } = FileManagerViewMode.List;\n    [Parameter] public EventCallback<FileManagerViewMode> ViewModeChanged { get; set; }\n\n    [Parameter] public FileSystemNode? SelectedNode { get; set; }\n    [Parameter] public EventCallback<FileSystemNode?> SelectedNodeChanged { get; set; }\n\n    [Parameter] public EventCallback<FileSystemNode> OnFileOpen { get; set; }\n\n    [Parameter] public EventCallback<(FileSystemNode Node, string NewName)> OnRename { get; set; }\n    [Parameter] public EventCallback<IReadOnlyList<FileSystemNode>> OnDelete { get; set; }\n    [Parameter] public EventCallback<FileSystemNode> OnNewFolder { get; set; }\n\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // State\n    // ──────────────────────────────────────────────────────────────────────────\n\n    // Navigation stack: list of ancestor nodes from root to current folder\n    private List<FileSystemNode> _pathStack = [];\n    private bool _loadingFolder;\n    // Which nodes currently have their children expanded in the tree pane\n    private readonly HashSet<string> _expandedTreeNodes = [];\n    // Inline-rename state\n    private FileSystemNode? _renamingNode;\n    private string _renameValue = \"\";\n    // Context menu state\n    private FileSystemNode? _contextMenuNode;\n    private bool _contextMenuOpen;\n    private double _contextMenuX;\n    private double _contextMenuY;\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // CSS helpers\n    // ──────────────────────────────────────────────────────────────────────────\n\n    private string CssClass => $\"flex flex-col rounded-lg border border-border/40 bg-card overflow-hidden h-full min-h-[400px] {Class}\".Trim();\n    private const string ToolbarButtonClass = \"inline-flex items-center justify-center h-7 w-7 rounded-md transition-colors hover:bg-accent text-muted-foreground hover:text-accent-foreground\";\n    private const string ToolbarButtonActiveClass = \"inline-flex items-center justify-center h-7 w-7 rounded-md transition-colors bg-accent text-accent-foreground\";\n    private const string BreadcrumbLinkClass = \"transition-colors hover:text-foreground text-muted-foreground rounded focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\";\n    private const string UpButtonClass = \"inline-flex items-center justify-center h-7 w-7 rounded-md transition-colors hover:bg-accent text-muted-foreground hover:text-accent-foreground disabled:opacity-40 disabled:pointer-events-none\";\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // Lifecycle\n    // ──────────────────────────────────────────────────────────────────────────\n\n    protected override async Task OnParametersSetAsync()\n    {\n        // Sync _pathStack when CurrentPath is changed externally\n        if (CurrentPath is null)\n        {\n            _pathStack = [];\n            return;\n        }\n\n        // If path stack already ends at this id, nothing to do\n        if (_pathStack.Count > 0 && _pathStack[^1].Id == CurrentPath)\n            return;\n\n        // Rebuild stack by finding the path in the tree\n        var newStack = new List<FileSystemNode>();\n        if (FindPath(Root ?? [], CurrentPath, newStack))\n            _pathStack = newStack;\n\n        // Ensure current folder's children are loaded\n        if (_pathStack.Count > 0)\n        {\n            var current = _pathStack[^1];\n            if (current.IsFolder && current.Children is null && LoadChildren is not null)\n            {\n                _loadingFolder = true;\n                try\n                {\n                    current.Children = await LoadChildren(current);\n                }\n                finally\n                {\n                    _loadingFolder = false;\n                }\n            }\n        }\n    }\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // Navigation helpers\n    // ──────────────────────────────────────────────────────────────────────────\n\n    private List<FileSystemNode> GetCurrentFolderContents()\n    {\n        if (_pathStack.Count == 0)\n            return (Root ?? []).ToList();\n\n        var current = _pathStack[^1];\n        return current.Children ?? [];\n    }\n\n    private static bool FindPath(IEnumerable<FileSystemNode> nodes, string targetId, List<FileSystemNode> stack)\n    {\n        foreach (var node in nodes)\n        {\n            if (node.Id == targetId)\n            {\n                stack.Add(node);\n                return true;\n            }\n            if (node.IsFolder && node.Children is { Count: > 0 })\n            {\n                stack.Add(node);\n                if (FindPath(node.Children, targetId, stack))\n                    return true;\n                stack.RemoveAt(stack.Count - 1);\n            }\n        }\n        return false;\n    }\n\n    private async Task NavigateToFolder(FileSystemNode folder)\n    {\n        // Build new path stack up to this folder\n        var newStack = new List<FileSystemNode>();\n        if (!FindPath(Root ?? [], folder.Id, newStack))\n            return;\n\n        _pathStack = newStack;\n\n        // Lazy-load children if needed\n        if (folder.Children is null && LoadChildren is not null)\n        {\n            _loadingFolder = true;\n            StateHasChanged();\n            try\n            {\n                folder.Children = await LoadChildren(folder);\n            }\n            finally\n            {\n                _loadingFolder = false;\n            }\n        }\n\n        // Expand in tree pane\n        _expandedTreeNodes.Add(folder.Id);\n\n        CurrentPath = folder.Id;\n        await CurrentPathChanged.InvokeAsync(CurrentPath);\n    }\n\n    private async Task NavigateToStackDepth(int depth)\n    {\n        if (depth < 0)\n        {\n            // Navigate to root\n            _pathStack = [];\n            CurrentPath = null;\n            await CurrentPathChanged.InvokeAsync(null);\n            return;\n        }\n\n        _pathStack = _pathStack.Take(depth + 1).ToList();\n        CurrentPath = _pathStack[^1].Id;\n        await CurrentPathChanged.InvokeAsync(CurrentPath);\n    }\n\n    private async Task NavigateUp()\n    {\n        if (_pathStack.Count == 0) return;\n        await NavigateToStackDepth(_pathStack.Count - 2);\n    }\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // View mode\n    // ──────────────────────────────────────────────────────────────────────────\n\n    private async Task SetViewMode(FileManagerViewMode mode)\n    {\n        ViewMode = mode;\n        await ViewModeChanged.InvokeAsync(mode);\n    }\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // Selection\n    // ──────────────────────────────────────────────────────────────────────────\n\n    private async Task SelectNode(FileSystemNode node)\n    {\n        SelectedNode = node;\n        await SelectedNodeChanged.InvokeAsync(node);\n    }\n\n    private async Task HandleNodeDoubleClick(FileSystemNode node)\n    {\n        if (node.IsFolder)\n        {\n            await NavigateToFolder(node);\n        }\n        else\n        {\n            await SelectNode(node);\n            await OnFileOpen.InvokeAsync(node);\n        }\n    }\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // Context menu / toolbar operations\n    // ──────────────────────────────────────────────────────────────────────────\n\n    private void OpenContextMenu(FileSystemNode node, MouseEventArgs e)\n    {\n        _contextMenuNode = node;\n        _contextMenuX = e.ClientX;\n        _contextMenuY = e.ClientY;\n        _contextMenuOpen = true;\n    }\n\n    private void CloseContextMenu()\n    {\n        _contextMenuOpen = false;\n        _contextMenuNode = null;\n    }\n\n    private void BeginRename(FileSystemNode node)\n    {\n        CloseContextMenu();\n        _renamingNode = node;\n        _renameValue = node.Name;\n    }\n\n    private async Task CommitRename()\n    {\n        if (_renamingNode is null) return;\n        var node = _renamingNode;\n        var newName = _renameValue.Trim();\n        _renamingNode = null;\n        _renameValue = \"\";\n\n        if (!string.IsNullOrWhiteSpace(newName) && newName != node.Name)\n            await OnRename.InvokeAsync((node, newName));\n    }\n\n    private void CancelRename()\n    {\n        _renamingNode = null;\n        _renameValue = \"\";\n    }\n\n    private async Task HandleDeleteNode(FileSystemNode node)\n    {\n        CloseContextMenu();\n        await OnDelete.InvokeAsync([node]);\n    }\n\n    private async Task HandleDeleteSelected()\n    {\n        if (SelectedNode is null) return;\n        await OnDelete.InvokeAsync([SelectedNode]);\n    }\n\n    private async Task HandleNewFolder()\n    {\n        var currentFolder = _pathStack.Count > 0 ? _pathStack[^1] : null;\n        await OnNewFolder.InvokeAsync(currentFolder ?? new FileSystemNode { Id = \"\", Name = \"Root\", IsFolder = true });\n    }\n\n    private async Task HandleOpenNode(FileSystemNode node)\n    {\n        CloseContextMenu();\n        if (node.IsFolder)\n            await NavigateToFolder(node);\n        else\n            await OnFileOpen.InvokeAsync(node);\n    }\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // Tree toggle\n    // ──────────────────────────────────────────────────────────────────────────\n\n    private async Task ToggleTreeFolder(FileSystemNode folder)\n    {\n        if (_expandedTreeNodes.Contains(folder.Id))\n        {\n            _expandedTreeNodes.Remove(folder.Id);\n        }\n        else\n        {\n            _expandedTreeNodes.Add(folder.Id);\n\n            if (folder.Children is null && LoadChildren is not null)\n            {\n                _loadingFolder = true;\n                StateHasChanged();\n                try\n                {\n                    folder.Children = await LoadChildren(folder);\n                }\n                finally\n                {\n                    _loadingFolder = false;\n                }\n            }\n        }\n    }\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // Renderers\n    // ──────────────────────────────────────────────────────────────────────────\n\n    private RenderFragment RenderTreeNode(FileSystemNode node, int level) => builder =>\n    {\n        if (!node.IsFolder) return;\n\n        bool expanded = _expandedTreeNodes.Contains(node.Id);\n        bool isCurrentFolder = _pathStack.Count > 0 && _pathStack[^1].Id == node.Id;\n        bool hasChildren = node.Children is { Count: > 0 } || (LoadChildren is not null && node.Children is null);\n\n        var rowClass = $\"flex items-center gap-1.5 py-1 px-2 rounded-md text-sm cursor-pointer transition-colors {(isCurrentFolder ? \"bg-accent text-accent-foreground\" : \"hover:bg-accent/50 text-foreground\")}\";\n\n        builder.OpenElement(0, \"div\");\n        builder.AddAttribute(1, \"role\", \"treeitem\");\n        builder.AddAttribute(2, \"aria-expanded\", expanded.ToString().ToLowerInvariant());\n        builder.AddAttribute(3, \"aria-selected\", isCurrentFolder.ToString().ToLowerInvariant());\n\n        builder.OpenElement(10, \"div\");\n        builder.AddAttribute(11, \"class\", rowClass);\n        builder.AddAttribute(12, \"style\", $\"padding-left: {level * 12 + 4}px\");\n        builder.AddAttribute(13, \"onclick\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, _ => NavigateToFolder(node)));\n\n        // Chevron\n        builder.OpenElement(20, \"button\");\n        builder.AddAttribute(21, \"type\", \"button\");\n        builder.AddAttribute(22, \"class\", \"shrink-0 w-4 h-4 flex items-center justify-center text-muted-foreground\");\n        builder.AddAttribute(23, \"onclick\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, async (_evt) =>\n        {\n            await ToggleTreeFolder(node);\n        }));\n        builder.AddAttribute(24, \"onclick:stopPropagation\", true);\n        builder.AddAttribute(25, \"aria-label\", expanded ? \"Collapse\" : \"Expand\");\n        if (hasChildren)\n        {\n            builder.OpenComponent<Blazicon>(30);\n            builder.AddAttribute(31, \"Svg\", expanded ? Lucide.ChevronDown : Lucide.ChevronRight);\n            builder.AddAttribute(32, \"class\", \"h-3 w-3\");\n            builder.CloseComponent();\n        }\n        builder.CloseElement(); // button\n\n        // Folder icon\n        builder.OpenComponent<Blazicon>(40);\n        builder.AddAttribute(41, \"Svg\", expanded ? Lucide.FolderOpen : Lucide.Folder);\n        builder.AddAttribute(42, \"class\", \"h-4 w-4 shrink-0 text-muted-foreground\");\n        builder.CloseComponent();\n\n        // Name\n        builder.OpenElement(50, \"span\");\n        builder.AddAttribute(51, \"class\", \"truncate\");\n        builder.AddContent(52, node.Name);\n        builder.CloseElement();\n\n        builder.CloseElement(); // row div\n\n        // Children\n        if (expanded && node.Children is { Count: > 0 })\n        {\n            builder.OpenElement(60, \"div\");\n            builder.AddAttribute(61, \"role\", \"group\");\n            foreach (var child in node.Children.Where(c => c.IsFolder))\n            {\n                var c = child;\n                builder.AddContent(62, RenderTreeNode(c, level + 1));\n            }\n            builder.CloseElement();\n        }\n\n        builder.CloseElement(); // treeitem div\n    };\n\n    private RenderFragment RenderListView(List<FileSystemNode> items) => builder =>\n    {\n        builder.OpenElement(0, \"table\");\n        builder.AddAttribute(1, \"class\", \"w-full text-sm border-collapse\");\n        builder.AddAttribute(2, \"role\", \"grid\");\n        builder.AddAttribute(3, \"aria-label\", \"File list\");\n\n        // thead\n        builder.OpenElement(10, \"thead\");\n        builder.OpenElement(11, \"tr\");\n        builder.AddAttribute(12, \"class\", \"border-b border-border/40 text-muted-foreground\");\n        AppendTh(builder, 13, \"Name\", \"text-left p-2 font-medium w-full\");\n        AppendTh(builder, 14, \"Size\", \"text-right p-2 font-medium whitespace-nowrap w-28\");\n        AppendTh(builder, 15, \"Modified\", \"text-right p-2 font-medium whitespace-nowrap w-40\");\n        AppendTh(builder, 16, \"\", \"w-8 p-2\"); // context menu column\n        builder.CloseElement(); // tr\n        builder.CloseElement(); // thead\n\n        // tbody\n        builder.OpenElement(20, \"tbody\");\n        foreach (var node in items)\n        {\n            var n = node;\n            bool isSelected = SelectedNode?.Id == n.Id;\n            var rowClass = $\"group border-b border-border/20 cursor-pointer transition-colors {(isSelected ? \"bg-accent\" : \"hover:bg-muted/40\")}\";\n\n            builder.OpenElement(30, \"tr\");\n            builder.AddAttribute(31, \"class\", rowClass);\n            builder.AddAttribute(32, \"role\", \"row\");\n            builder.AddAttribute(33, \"aria-selected\", isSelected.ToString().ToLowerInvariant());\n            builder.AddAttribute(34, \"onclick\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, _ => SelectNode(n)));\n            builder.AddAttribute(35, \"ondblclick\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, _ => HandleNodeDoubleClick(n)));\n            builder.AddAttribute(36, \"oncontextmenu\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, e => OpenContextMenu(n, e)));\n            builder.AddAttribute(37, \"oncontextmenu:preventDefault\", true);\n\n            // Name cell\n            builder.OpenElement(40, \"td\");\n            builder.AddAttribute(41, \"class\", \"p-2\");\n            builder.OpenElement(42, \"div\");\n            builder.AddAttribute(43, \"class\", \"flex items-center gap-2\");\n\n            // File icon\n            builder.OpenComponent<Blazicon>(50);\n            builder.AddAttribute(51, \"Svg\", GetNodeIcon(n));\n            builder.AddAttribute(52, \"class\", \"h-4 w-4 shrink-0 text-muted-foreground\");\n            builder.CloseComponent();\n\n            // Inline rename or plain text\n            if (_renamingNode?.Id == n.Id)\n            {\n                builder.OpenElement(60, \"input\");\n                builder.AddAttribute(61, \"type\", \"text\");\n                builder.AddAttribute(62, \"class\", \"flex-1 px-1 py-0 text-sm border border-ring rounded bg-background focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring\");\n                builder.AddAttribute(63, \"value\", _renameValue);\n                builder.AddAttribute(64, \"oninput\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.ChangeEventArgs>(this, e => _renameValue = e.Value?.ToString() ?? \"\"));\n                builder.AddAttribute(65, \"onblur\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.FocusEventArgs>(this, _ => CommitRename()));\n                builder.AddAttribute(66, \"onkeydown\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.KeyboardEventArgs>(this, async e =>\n                {\n                    if (e.Key == \"Enter\") await CommitRename();\n                    else if (e.Key == \"Escape\") CancelRename();\n                }));\n                builder.AddAttribute(67, \"onclick:stopPropagation\", true);\n                builder.CloseElement();\n            }\n            else\n            {\n                builder.OpenElement(70, \"span\");\n                builder.AddAttribute(71, \"class\", \"truncate\");\n                builder.AddContent(72, n.Name);\n                builder.CloseElement();\n            }\n\n            builder.CloseElement(); // inner div\n            builder.CloseElement(); // td name\n\n            // Size cell\n            builder.OpenElement(80, \"td\");\n            builder.AddAttribute(81, \"class\", \"p-2 text-right text-muted-foreground whitespace-nowrap\");\n            builder.AddContent(82, n.IsFolder ? \"—\" : FormatSize(n.Size ?? 0));\n            builder.CloseElement();\n\n            // Modified cell\n            builder.OpenElement(90, \"td\");\n            builder.AddAttribute(91, \"class\", \"p-2 text-right text-muted-foreground whitespace-nowrap\");\n            builder.AddContent(92, n.Modified.HasValue ? n.Modified.Value.ToString(\"yyyy-MM-dd HH:mm\") : \"—\");\n            builder.CloseElement();\n\n            // Context menu cell\n            builder.OpenElement(100, \"td\");\n            builder.AddAttribute(101, \"class\", \"p-2\");\n            builder.OpenElement(102, \"button\");\n            builder.AddAttribute(103, \"type\", \"button\");\n            builder.AddAttribute(104, \"class\", \"inline-flex items-center justify-center h-6 w-6 rounded opacity-0 group-hover:opacity-100 transition-opacity hover:bg-accent text-muted-foreground hover:text-accent-foreground\");\n            builder.AddAttribute(105, \"title\", L[\"FileManager.MoreActions\"]);\n            builder.AddAttribute(106, \"aria-label\", L[\"FileManager.MoreActionsForName\", n.Name]);\n            builder.AddAttribute(107, \"onclick\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, e => OpenContextMenu(n, e)));\n            builder.AddAttribute(108, \"onclick:stopPropagation\", true);\n            builder.OpenComponent<Blazicon>(109);\n            builder.AddAttribute(110, \"Svg\", Lucide.EllipsisVertical);\n            builder.AddAttribute(111, \"class\", \"h-3.5 w-3.5\");\n            builder.CloseComponent();\n            builder.CloseElement(); // button\n            builder.CloseElement(); // td\n\n            builder.CloseElement(); // tr\n        }\n        builder.CloseElement(); // tbody\n        builder.CloseElement(); // table\n    };\n\n    private RenderFragment RenderGridView(List<FileSystemNode> items) => builder =>\n    {\n        builder.OpenElement(0, \"div\");\n        builder.AddAttribute(1, \"class\", \"grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-2 p-2\");\n        builder.AddAttribute(2, \"role\", \"listbox\");\n        builder.AddAttribute(3, \"aria-label\", \"File grid\");\n\n        foreach (var node in items)\n        {\n            var n = node;\n            bool isSelected = SelectedNode?.Id == n.Id;\n            var tileClass = $\"flex flex-col items-center gap-2 p-3 rounded-lg cursor-pointer transition-colors border {(isSelected ? \"bg-accent border-ring\" : \"border-transparent hover:bg-muted/40 hover:border-border/40\")}\";\n\n            builder.OpenElement(10, \"div\");\n            builder.AddAttribute(11, \"class\", tileClass);\n            builder.AddAttribute(12, \"role\", \"option\");\n            builder.AddAttribute(13, \"aria-selected\", isSelected.ToString().ToLowerInvariant());\n            builder.AddAttribute(14, \"aria-label\", n.Name);\n            builder.AddAttribute(15, \"onclick\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, _ => SelectNode(n)));\n            builder.AddAttribute(16, \"ondblclick\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, _ => HandleNodeDoubleClick(n)));\n            builder.AddAttribute(17, \"oncontextmenu\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, e => OpenContextMenu(n, e)));\n            builder.AddAttribute(18, \"oncontextmenu:preventDefault\", true);\n\n            // Icon\n            builder.OpenComponent<Blazicon>(20);\n            builder.AddAttribute(21, \"Svg\", GetNodeIcon(n));\n            builder.AddAttribute(22, \"class\", \"h-10 w-10 text-muted-foreground\");\n            builder.CloseComponent();\n\n            // Name\n            builder.OpenElement(30, \"span\");\n            builder.AddAttribute(31, \"class\", \"text-xs text-center truncate w-full leading-tight\");\n            builder.AddAttribute(32, \"title\", n.Name);\n            builder.AddContent(33, n.Name);\n            builder.CloseElement();\n\n            builder.CloseElement(); // tile\n        }\n\n        builder.CloseElement(); // grid div\n    };\n\n    private static void AppendTh(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder, int seq, string text, string cls)\n    {\n        builder.OpenElement(seq, \"th\");\n        builder.AddAttribute(seq + 1, \"class\", cls);\n        builder.AddAttribute(seq + 2, \"scope\", \"col\");\n        builder.AddContent(seq + 3, text);\n        builder.CloseElement();\n    }\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // Icon resolution\n    // ──────────────────────────────────────────────────────────────────────────\n\n    private static SvgIcon GetNodeIcon(FileSystemNode node)\n    {\n        if (!string.IsNullOrEmpty(node.IconName))\n        {\n            // Try to look up by name via reflection — fall through to defaults if missing\n            var prop = typeof(Lucide).GetProperty(node.IconName,\n                System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);\n            if (prop?.GetValue(null) is SvgIcon icon) return icon;\n        }\n\n        if (node.IsFolder) return Lucide.Folder;\n\n        var ext = System.IO.Path.GetExtension(node.Name).ToLowerInvariant();\n        return ext switch\n        {\n            \".png\" or \".jpg\" or \".jpeg\" or \".gif\" or \".svg\" or \".webp\" or \".bmp\" or \".ico\" => Lucide.Image,\n            \".pdf\" => Lucide.FileText,\n            \".zip\" or \".tar\" or \".gz\" or \".rar\" or \".7z\" or \".bz2\" => Lucide.FileArchive,\n            \".mp3\" or \".wav\" or \".ogg\" or \".flac\" or \".aac\" or \".m4a\" => Lucide.Music,\n            \".mp4\" or \".mov\" or \".avi\" or \".mkv\" or \".webm\" or \".wmv\" => Lucide.Video,\n            \".txt\" or \".md\" or \".log\" or \".rst\" or \".rtf\" => Lucide.FileText,\n            \".cs\" or \".js\" or \".ts\" or \".jsx\" or \".tsx\" or \".razor\" or \".json\" or \".xml\"\n                or \".html\" or \".htm\" or \".css\" or \".scss\" or \".less\" or \".yaml\" or \".yml\"\n                or \".toml\" or \".ini\" or \".sh\" or \".bash\" or \".ps1\" or \".py\" or \".rb\"\n                or \".java\" or \".cpp\" or \".c\" or \".h\" or \".go\" or \".rs\" or \".swift\" => Lucide.FileCode,\n            _ => Lucide.File\n        };\n    }\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // Size formatting\n    // ──────────────────────────────────────────────────────────────────────────\n\n    private static string FormatSize(long bytes)\n    {\n        if (bytes < 1024) return $\"{bytes} B\";\n        if (bytes < 1024 * 1024) return $\"{bytes / 1024.0:F1} KB\";\n        if (bytes < 1024L * 1024 * 1024) return $\"{bytes / (1024.0 * 1024):F1} MB\";\n        return $\"{bytes / (1024.0 * 1024 * 1024):F1} GB\";\n    }\n\n    // ──────────────────────────────────────────────────────────────────────────\n    // Enums\n    // ──────────────────────────────────────────────────────────────────────────\n\n    public enum FileManagerViewMode\n    {\n        List,\n        Grid\n    }\n}\n\n@* ── Context menu overlay ───────────────────────────────────────────────── *@\n@if (_contextMenuOpen && _contextMenuNode is not null)\n{\n    var node = _contextMenuNode;\n    <div class=\"fixed inset-0 z-40\" @onclick=\"CloseContextMenu\" aria-hidden=\"true\"></div>\n    <div class=\"fixed z-50 min-w-[10rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg\"\n         style=\"@($\"left:{_contextMenuX.ToString(System.Globalization.CultureInfo.InvariantCulture)}px;top:{_contextMenuY.ToString(System.Globalization.CultureInfo.InvariantCulture)}px\")\"\n         role=\"menu\"\n         aria-label=\"File actions\">\n        @if (node.IsFolder)\n        {\n            <button type=\"button\" class=\"@MenuItemClass\" role=\"menuitem\" @onclick=\"() => HandleOpenNode(node)\">\n                <Blazicon Svg=\"Lucide.FolderOpen\" class=\"h-4 w-4\" />\n                Open\n            </button>\n        }\n        else\n        {\n            <button type=\"button\" class=\"@MenuItemClass\" role=\"menuitem\" @onclick=\"() => HandleOpenNode(node)\">\n                <Blazicon Svg=\"Lucide.ExternalLink\" class=\"h-4 w-4\" />\n                Open\n            </button>\n        }\n        @if (OnRename.HasDelegate)\n        {\n            <button type=\"button\" class=\"@MenuItemClass\" role=\"menuitem\" @onclick=\"() => BeginRename(node)\">\n                <Blazicon Svg=\"Lucide.Pencil\" class=\"h-4 w-4\" />\n                Rename\n            </button>\n        }\n        @if (OnDelete.HasDelegate)\n        {\n            <div class=\"my-0.5 h-px bg-border/40\"></div>\n            <button type=\"button\" class=\"@MenuItemDeleteClass\" role=\"menuitem\" @onclick=\"() => HandleDeleteNode(node)\">\n                <Blazicon Svg=\"Lucide.Trash2\" class=\"h-4 w-4\" />\n                Delete\n            </button>\n        }\n    </div>\n}\n\n@code {\n    private const string MenuItemClass = \"relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\";\n    private const string MenuItemDeleteClass = \"relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-destructive/10 hover:text-destructive text-destructive\";\n}\n"
    },
    {
      "path": "UI/FileManager/FileManagerModel.cs",
      "content": "namespace Lumeo;\n\n/// <summary>\n/// Represents a file or folder node in the FileManager tree.\n/// </summary>\npublic class FileSystemNode\n{\n    /// <summary>Unique identifier for this node. Used as the navigation key for CurrentPath.</summary>\n    public string Id { get; set; } = \"\";\n\n    /// <summary>Display name (file or folder name).</summary>\n    public string Name { get; set; } = \"\";\n\n    /// <summary>True if this node is a folder; false if it is a file.</summary>\n    public bool IsFolder { get; set; }\n\n    /// <summary>File size in bytes. Null for folders.</summary>\n    public long? Size { get; set; }\n\n    /// <summary>Last-modified timestamp.</summary>\n    public DateTime? Modified { get; set; }\n\n    /// <summary>\n    /// Full display path (optional). If not provided, the FileManager derives\n    /// path breadcrumbs from the tree structure.\n    /// </summary>\n    public string? Path { get; set; }\n\n    /// <summary>\n    /// Child nodes. Non-null means children are already loaded.\n    /// Null means lazy-loading is possible (if <c>LoadChildren</c> is set).\n    /// An empty list means an empty folder.\n    /// </summary>\n    public List<FileSystemNode>? Children { get; set; }\n\n    /// <summary>\n    /// Optional Lucide icon name override. When set, overrides the\n    /// automatic icon selection based on file extension.\n    /// </summary>\n    public string? IconName { get; set; }\n\n    /// <summary>Arbitrary consumer payload attached to this node.</summary>\n    public object? Tag { get; set; }\n}\n"
    }
  ],
  "slots": [],
  "serviceDependencies": [
    {
      "service": "ILumeoLocalizer",
      "namespace": "Lumeo.Services.Localization"
    }
  ],
  "cascadingDependencies": [],
  "relatedComponents": [
    {
      "name": "List",
      "reason": "used internally"
    }
  ],
  "keyboardInteractions": [
    {
      "key": "Enter",
      "action": "ToggleTreeFolder handles Enter in FileManager.razor"
    },
    {
      "key": "Escape",
      "action": "ToggleTreeFolder handles Escape in FileManager.razor"
    }
  ],
  "tests": [
    "tests/Lumeo.Tests/Components/FileManager/FileManagerTests.cs"
  ],
  "mdSummary": "# FileManager\n\nHeadless file and folder explorer — folder tree, breadcrumb path, list/grid views, lazy loading, inline rename, context-menu operations.\n\n**Package:** `Lumeo` · **Category:** Data Display\n**Docs:** https://lumeo.nativ.sh/components/file-manager\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| FileManager | Root | `IReadOnlyList<FileSystemNode>?` | `` |  |\n| FileManager | CurrentPath | `string?` | `` | The ID of the currently open folder. Null means the root level. Uses Id as the path identifier. |\n| FileManager | CurrentPathChanged | `EventCallback<string?>` | `` |  |\n| FileManager | LoadChildren | `Func<FileSystemNode, Task<List<FileSystemNode>>>?` | `` | Called the first time a folder is opened if its Children is null. Return the loaded children. |\n| FileManager | ViewMode | `FileManagerViewMode` | `FileManagerViewMode.List` |  |\n| FileManager | ViewModeChanged | `EventCallback<FileManagerViewMode>` | `` |  |\n| FileManager | SelectedNode | `FileSystemNode?` | `` |  |\n| FileManager | SelectedNodeChanged | `EventCallback<FileSystemNode?>` | `` |  |\n| FileManager | OnFileOpen | `EventCallback<FileSystemNode>` | `` |  |\n| FileManager | OnRename | `EventCallback<(FileSystemNode Node, string NewName)>` | `` |  |\n| FileManager | OnDelete | `EventCallback<IReadOnlyList<FileSystemNode>>` | `` |  |\n| FileManager | OnNewFolder | `EventCallback<FileSystemNode>` | `` |  |\n| FileManager | Class | `string?` | `` |  |\n| FileManager | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- (none)\n\n## Events\n- `CurrentPathChanged` (`EventCallback<string?>`)\n- `ViewModeChanged` (`EventCallback<FileManagerViewMode>`)\n- `SelectedNodeChanged` (`EventCallback<FileSystemNode?>`)\n- `OnFileOpen` (`EventCallback<FileSystemNode>`)\n- `OnRename` (`EventCallback<(FileSystemNode Node, string NewName)>`)\n- `OnDelete` (`EventCallback<IReadOnlyList<FileSystemNode>>`)\n- `OnNewFolder` (`EventCallback<FileSystemNode>`)\n\n## Sub-components\n- (none)\n\n## Cascading values it expects\n- (none)\n\n## Services it injects\n- `ILumeoLocalizer` (Lumeo.Services.Localization)\n\n## Keyboard\n- `Enter` — ToggleTreeFolder handles Enter in FileManager.razor\n- `Escape` — ToggleTreeFolder handles Escape in FileManager.razor\n\n## Example\n```razor\n<FileManager />\n```\n\n## CSS variables\n`--color-accent`, `--color-accent-foreground`, `--color-background`, `--color-border`, `--color-card`, `--color-destructive`, `--color-foreground`, `--color-muted`, `--color-muted-foreground`, `--color-popover`, `--color-popover-foreground`, `--color-ring`, `--radius`\n\n## Files\n- `UI/FileManager/FileManager.razor`\n- `UI/FileManager/FileManagerModel.cs`\n"
}