{
  "name": "FileUpload",
  "category": "Forms",
  "subcategory": "Specialized",
  "description": "Drag-and-drop file dropzone with progress and validation.",
  "thumbnail": "/preview-cards/file-upload.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/FileUpload/FileUpload.razor"
  ],
  "dependencies": [
    "input"
  ],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-background",
    "--color-border",
    "--color-destructive",
    "--color-input",
    "--color-muted",
    "--color-muted-foreground",
    "--color-primary",
    "--color-primary-foreground",
    "--radius"
  ],
  "api": {
    "name": "FileUpload",
    "category": "Forms",
    "subcategory": "Specialized",
    "description": "Drag-and-drop file dropzone with progress and validation.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/FileUpload/FileUpload.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IAsyncDisposable"
    ],
    "parameters": [
      {
        "name": "FormField",
        "type": "FormField.FormFieldContext?",
        "default": null,
        "description": null,
        "isCascading": true,
        "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": "Name",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Label",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Description",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Multiple",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Accept",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "MaxFileSize",
        "type": "long",
        "default": "10 * 1024 * 1024",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "MaxFiles",
        "type": "int",
        "default": "10",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ShowThumbnails",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "FileTemplate",
        "type": "RenderFragment<IBrowserFile>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Variant",
        "type": "FileUploadVariant",
        "default": "FileUploadVariant.Dropzone",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Size",
        "type": "FileUploadSize",
        "default": "FileUploadSize.Default",
        "description": "Size of the trigger when Variant=\"Button\". Mirrors ButtonSize so a button-variant FileUpload can match neighbouring buttons in toolbars or compact forms. Has no visual effect on Dropzone or Avatar variants today.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ShowProgress",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Progress",
        "type": "int",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnUpload",
        "type": "Func<FileUploadItem, IProgress<int>, CancellationToken, Task<string?>>?",
        "default": null,
        "description": "Consumer-supplied upload handler. Called once per file after selection (or after explicit StartUploadAsync if AutoUpload is false). Receives the item, an IProgress{T} for reporting percent 0-100, and a CancellationToken that fires if the user clicks Cancel. Should return the server-assigned URL/id on success, or throw/return null on failure.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "AutoUpload",
        "type": "bool",
        "default": "true",
        "description": "When true (default), uploads begin immediately after files are selected. When false, the user must click the Upload button to start. Requires OnUpload to be set.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "MaxConcurrentUploads",
        "type": "int",
        "default": "3",
        "description": "Maximum number of OnUpload calls running simultaneously.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ChunkSize",
        "type": "long?",
        "default": null,
        "description": "Optional chunk size hint (bytes) for consumers that want to implement chunked/resumable upload inside their OnUpload handler. The component passes this value through ChunkSize but does NOT implement chunking itself.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "MaxFileSizeError",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "MaxFilesError",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "FileTypeError",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnFilesSelected",
        "type": "EventCallback<InputFileChangeEventArgs>",
        "default": null,
        "description": "Fires when files are selected (raw args, kept for back-compat).",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnFileUploaded",
        "type": "EventCallback<FileUploadItem>",
        "default": null,
        "description": "Fires each time a file upload succeeds.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnAllUploaded",
        "type": "EventCallback<IReadOnlyList<FileUploadItem>>",
        "default": null,
        "description": "Fires when all queued uploads have finished (succeeded, failed, or cancelled).",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnFileFailed",
        "type": "EventCallback<FileUploadItem>",
        "default": null,
        "description": "Fires each time a file upload fails.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnFileRemoved",
        "type": "EventCallback<FileUploadItem>",
        "default": null,
        "description": "Fires when a file is removed from the list.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnFileRejected",
        "type": "EventCallback<(IBrowserFile File, string Reason)>",
        "default": null,
        "description": "Fires when a file is rejected due to type/size/count constraints.",
        "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": "OnFilesSelected",
        "type": "EventCallback<InputFileChangeEventArgs>",
        "description": "Fires when files are selected (raw args, kept for back-compat)."
      },
      {
        "name": "OnFileUploaded",
        "type": "EventCallback<FileUploadItem>",
        "description": "Fires each time a file upload succeeds."
      },
      {
        "name": "OnAllUploaded",
        "type": "EventCallback<IReadOnlyList<FileUploadItem>>",
        "description": "Fires when all queued uploads have finished (succeeded, failed, or cancelled)."
      },
      {
        "name": "OnFileFailed",
        "type": "EventCallback<FileUploadItem>",
        "description": "Fires each time a file upload fails."
      },
      {
        "name": "OnFileRemoved",
        "type": "EventCallback<FileUploadItem>",
        "description": "Fires when a file is removed from the list."
      },
      {
        "name": "OnFileRejected",
        "type": "EventCallback<(IBrowserFile File, string Reason)>",
        "description": "Fires when a file is rejected due to type/size/count constraints."
      }
    ],
    "enums": [
      {
        "name": "FileUploadVariant",
        "values": [
          "Dropzone",
          "Button",
          "Avatar"
        ],
        "description": null
      },
      {
        "name": "FileUploadSize",
        "values": [
          "Sm",
          "Default",
          "Lg"
        ],
        "description": null
      },
      {
        "name": "FileUploadStatus",
        "values": [
          "Pending",
          "Uploading",
          "Succeeded",
          "Failed",
          "Cancelled"
        ],
        "description": null
      }
    ],
    "records": [],
    "cssVars": [
      "--color-background",
      "--color-border",
      "--color-destructive",
      "--color-input",
      "--color-muted",
      "--color-muted-foreground",
      "--color-primary",
      "--color-primary-foreground",
      "--radius"
    ],
    "examples": [
      {
        "title": "Default",
        "code": "<FileUpload OnFilesSelected=\"HandleFiles\" />"
      },
      {
        "title": "Image Upload",
        "code": "<FileUpload\n    Label=\"Upload an image\"\n    Description=\"PNG, JPG or GIF up to 10MB\"\n    Accept=\"image/*\"\n    OnFilesSelected=\"HandleFiles\" />"
      },
      {
        "title": "Multiple Files",
        "code": "<FileUpload\n    Label=\"Upload documents\"\n    Description=\"Select one or more files\"\n    Multiple=\"true\"\n    OnFilesSelected=\"HandleFiles\" />"
      },
      {
        "title": "Upload with progress (fake delay)",
        "code": "<FileUpload\n    Multiple=\"true\"\n    Label=\"Upload files\"\n    OnUpload=\"@SimulateUpload\"\n    OnFileUploaded=\"@(item => _uploadedCount++)\"\n    OnAllUploaded=\"@(items => _allDoneMsg = $\"All {items.Count} file(s) done!\")\"  />\n\n@code {\n    private async Task<string?> SimulateUpload(\n        FileUpload.FileUploadItem item,\n        IProgress<int> progress,\n        CancellationToken ct)\n    {\n        for (int pct = 0; pct <= 100; pct += 10)\n        {\n            ct.ThrowIfCancellationRequested();\n            progress.Report(pct);\n            await Task.Delay(50, ct);\n        }\n        return $\"https://cdn.example.com/{item.Name}\";\n    }\n}"
      }
    ],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "file-upload",
  "docsUrl": "https://lumeo.nativ.sh/components/file-upload",
  "sourceUrl": [
    {
      "file": "UI/FileUpload/FileUpload.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/FileUpload/FileUpload.razor"
    }
  ],
  "source": [
    {
      "path": "UI/FileUpload/FileUpload.razor",
      "content": "@namespace Lumeo\n@implements IAsyncDisposable\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n@if (Label is not null && !InsideFormField && Variant != FileUploadVariant.Button && Variant != FileUploadVariant.Avatar)\n{\n    <label class=\"text-sm font-medium leading-none\">\n        @Label\n        @if (Required)\n        {\n            <span class=\"text-destructive ml-0.5\">*</span>\n        }\n    </label>\n}\n\n@if (Variant == FileUploadVariant.Button)\n{\n    <div class=\"@($\"inline-flex flex-col gap-2 {Class}\".Trim())\"\n         aria-required=\"@Required.ToString().ToLower()\"\n         aria-invalid=\"@EffectiveInvalid.ToString().ToLower()\"\n         @attributes=\"AdditionalAttributes\">\n        <label class=\"@ButtonLabelCssClass\">\n            <Blazicon Svg=\"Lucide.Upload\" class=\"h-4 w-4\" />\n            <span>@(Label ?? L[\"FileUpload.ChooseFile\"])</span>\n            <InputFile class=\"sr-only\" OnChange=\"HandleFileSelected\" multiple=\"@Multiple\" accept=\"@Accept\" />\n        </label>\n        @if (!AutoUpload && _uploadItems.Any(i => i.Status == FileUploadStatus.Pending))\n        {\n            <button type=\"button\" class=\"inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 transition-colors\"\n                    @onclick=\"StartUploadAsync\">\n                <Blazicon Svg=\"Lucide.Upload\" class=\"h-3.5 w-3.5\" />\n                Upload\n            </button>\n        }\n        @if (_globalErrorMessage is not null)\n        {\n            <p class=\"text-xs text-destructive\">@_globalErrorMessage</p>\n        }\n        @if (_uploadItems.Count > 0)\n        {\n            <div class=\"flex flex-col gap-1\">\n                @foreach (var item in _uploadItems)\n                {\n                    <div @key=\"item.Id\">\n                    @if (FileTemplate is not null)\n                    {\n                        @FileTemplate(item.File)\n                    }\n                    else\n                    {\n                        @RenderFileItem(item)\n                    }\n                    </div>\n                }\n            </div>\n        }\n        @if (!InsideFormField)\n        {\n            @if (EffectiveInvalid && ErrorText is not null)\n            {\n                <p class=\"text-xs text-destructive\">@ErrorText</p>\n            }\n            else if (HelperText is not null)\n            {\n                <p class=\"text-xs text-muted-foreground\">@HelperText</p>\n            }\n        }\n    </div>\n}\nelse if (Variant == FileUploadVariant.Avatar)\n{\n    <div class=\"@($\"inline-flex flex-col items-center gap-2 {Class}\".Trim())\"\n         aria-required=\"@Required.ToString().ToLower()\"\n         aria-invalid=\"@EffectiveInvalid.ToString().ToLower()\"\n         @attributes=\"AdditionalAttributes\">\n        <label class=\"flex h-20 w-20 cursor-pointer items-center justify-center overflow-hidden rounded-full border-2 border-dashed border-input bg-muted transition-colors hover:border-primary hover:bg-muted/80\">\n            @if (ShowThumbnails && _thumbnailUrl is not null)\n            {\n                <img src=\"@_thumbnailUrl\" alt=\"Avatar preview\" class=\"h-full w-full object-cover\" />\n            }\n            else\n            {\n                <Blazicon Svg=\"Lucide.User\" class=\"h-8 w-8 text-muted-foreground\" />\n            }\n            <InputFile class=\"sr-only\" OnChange=\"HandleFileSelected\" multiple=\"false\" accept=\"@(Accept ?? \"image/*\")\" />\n        </label>\n        @if (_globalErrorMessage is not null)\n        {\n            <p class=\"text-xs text-destructive\">@_globalErrorMessage</p>\n        }\n        @if (!InsideFormField)\n        {\n            @if (EffectiveInvalid && ErrorText is not null)\n            {\n                <p class=\"text-xs text-destructive\">@ErrorText</p>\n            }\n            else if (HelperText is not null)\n            {\n                <p class=\"text-xs text-muted-foreground\">@HelperText</p>\n            }\n        }\n    </div>\n}\nelse\n{\n    <div class=\"@($\"flex flex-col gap-2 {Class}\".Trim())\" @attributes=\"AdditionalAttributes\">\n        <label class=\"@DropzoneCssClass\"\n               aria-required=\"@Required.ToString().ToLower()\"\n               aria-invalid=\"@EffectiveInvalid.ToString().ToLower()\"\n               @ondragenter=\"HandleDragEnter\"\n               @ondragleave=\"HandleDragLeave\"\n               @ondragover:preventDefault=\"true\"\n               @ondrop=\"HandleDrop\">\n            <div class=\"flex flex-col items-center justify-center gap-2 py-4\">\n                <Blazicon Svg=\"Lucide.Upload\" class=\"h-8 w-8 text-muted-foreground\" />\n                <div class=\"text-center\">\n                    <p class=\"text-sm font-medium\">\n                        @if (Label is not null)\n                        {\n                            @Label\n                        }\n                        else\n                        {\n                            <span>@L[\"FileUpload.ClickToUpload\"]</span>\n                        }\n                    </p>\n                    @if (Description is not null)\n                    {\n                        <p class=\"text-xs text-muted-foreground mt-1\">@Description</p>\n                    }\n                </div>\n            </div>\n            <InputFile class=\"sr-only\" OnChange=\"HandleFileSelected\" multiple=\"@Multiple\" accept=\"@Accept\" />\n        </label>\n        @if (_globalErrorMessage is not null)\n        {\n            <p class=\"text-xs text-destructive\">@_globalErrorMessage</p>\n        }\n        @* Legacy global progress bar — kept for back-compat *@\n        @if (ShowProgress && OnUpload is null)\n        {\n            <div class=\"h-2 w-full overflow-hidden rounded-full bg-muted\">\n                <div class=\"h-full rounded-full bg-primary transition-all duration-300\"\n                     style=\"width: @(Math.Clamp(Progress, 0, 100))%\"></div>\n            </div>\n        }\n        @if (!AutoUpload && _uploadItems.Any(i => i.Status == FileUploadStatus.Pending))\n        {\n            <button type=\"button\" class=\"inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors self-start\"\n                    @onclick=\"StartUploadAsync\">\n                <Blazicon Svg=\"Lucide.Upload\" class=\"h-4 w-4\" />\n                Upload @(_uploadItems.Count(i => i.Status == FileUploadStatus.Pending)) file@(_uploadItems.Count(i => i.Status == FileUploadStatus.Pending) != 1 ? \"s\" : \"\")\n            </button>\n        }\n        @if (_uploadItems.Count > 0)\n        {\n            <div class=\"flex flex-col gap-2\">\n                @foreach (var item in _uploadItems)\n                {\n                    <div @key=\"item.Id\">\n                    @if (FileTemplate is not null)\n                    {\n                        @FileTemplate(item.File)\n                    }\n                    else\n                    {\n                        @RenderFileItem(item)\n                    }\n                    </div>\n                }\n            </div>\n        }\n        @if (!InsideFormField)\n        {\n            @if (EffectiveInvalid && ErrorText is not null)\n            {\n                <p class=\"text-xs text-destructive\">@ErrorText</p>\n            }\n            else if (HelperText is not null)\n            {\n                <p class=\"text-xs text-muted-foreground\">@HelperText</p>\n            }\n        }\n    </div>\n}\n\n@code {\n    // ── Form-field integration ───────────────────────────────────────────────\n    [CascadingParameter] private FormField.FormFieldContext? FormField { get; set; }\n\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? Name { get; set; }\n\n    private bool EffectiveInvalid => Invalid || FormField?.HasError == true;\n    private bool InsideFormField => FormField is not null;\n\n    // ── Visual / selection params ────────────────────────────────────────────\n    [Parameter] public string? Label { get; set; }\n    [Parameter] public string? Description { get; set; }\n    [Parameter] public bool Multiple { get; set; }\n    [Parameter] public string? Accept { get; set; }\n    [Parameter] public long MaxFileSize { get; set; } = 10 * 1024 * 1024;\n    [Parameter] public int MaxFiles { get; set; } = 10;\n    [Parameter] public bool ShowThumbnails { get; set; }\n    [Parameter] public RenderFragment<IBrowserFile>? FileTemplate { get; set; }\n    [Parameter] public FileUploadVariant Variant { get; set; } = FileUploadVariant.Dropzone;\n\n    /// <summary>\n    /// Size of the trigger when <c>Variant=\"Button\"</c>. Mirrors\n    /// <see cref=\"Lumeo.Button.ButtonSize\"/> so a button-variant FileUpload can\n    /// match neighbouring buttons in toolbars or compact forms. Has no visual\n    /// effect on <c>Dropzone</c> or <c>Avatar</c> variants today.\n    /// </summary>\n    [Parameter] public FileUploadSize Size { get; set; } = FileUploadSize.Default;\n\n    // ── Legacy back-compat progress params ──────────────────────────────────\n    [Parameter] public bool ShowProgress { get; set; }\n\n    /// <remarks>Obsolete when <see cref=\"OnUpload\"/> is set — per-file progress is tracked automatically via <see cref=\"FileUploadItem.ProgressPercent\"/>.</remarks>\n    [Parameter] public int Progress { get; set; }\n\n    // ── Upload pipeline params ───────────────────────────────────────────────\n    /// <summary>\n    /// Consumer-supplied upload handler. Called once per file after selection\n    /// (or after explicit <c>StartUploadAsync</c> if <see cref=\"AutoUpload\"/> is false).\n    /// Receives the item, an <see cref=\"IProgress{T}\"/> for reporting percent 0-100,\n    /// and a <see cref=\"CancellationToken\"/> that fires if the user clicks Cancel.\n    /// Should return the server-assigned URL/id on success, or throw/return null on failure.\n    /// </summary>\n    [Parameter] public Func<FileUploadItem, IProgress<int>, CancellationToken, Task<string?>>? OnUpload { get; set; }\n\n    /// <summary>\n    /// When <c>true</c> (default), uploads begin immediately after files are selected.\n    /// When <c>false</c>, the user must click the Upload button to start.\n    /// Requires <see cref=\"OnUpload\"/> to be set.\n    /// </summary>\n    [Parameter] public bool AutoUpload { get; set; } = true;\n\n    /// <summary>Maximum number of <see cref=\"OnUpload\"/> calls running simultaneously.</summary>\n    [Parameter] public int MaxConcurrentUploads { get; set; } = 3;\n\n    /// <summary>\n    /// Optional chunk size hint (bytes) for consumers that want to implement\n    /// chunked/resumable upload inside their <see cref=\"OnUpload\"/> handler.\n    /// The component passes this value through <see cref=\"FileUploadItem.ChunkSize\"/>\n    /// but does NOT implement chunking itself.\n    /// </summary>\n    [Parameter] public long? ChunkSize { get; set; }\n\n    // ── Validation error message overrides ──────────────────────────────────\n    [Parameter] public string? MaxFileSizeError { get; set; }\n    [Parameter] public string? MaxFilesError { get; set; }\n    [Parameter] public string? FileTypeError { get; set; }\n\n    // ── Events ───────────────────────────────────────────────────────────────\n    /// <summary>Fires when files are selected (raw args, kept for back-compat).</summary>\n    [Parameter] public EventCallback<InputFileChangeEventArgs> OnFilesSelected { get; set; }\n\n    /// <summary>Fires each time a file upload succeeds.</summary>\n    [Parameter] public EventCallback<FileUploadItem> OnFileUploaded { get; set; }\n\n    /// <summary>Fires when all queued uploads have finished (succeeded, failed, or cancelled).</summary>\n    [Parameter] public EventCallback<IReadOnlyList<FileUploadItem>> OnAllUploaded { get; set; }\n\n    /// <summary>Fires each time a file upload fails.</summary>\n    [Parameter] public EventCallback<FileUploadItem> OnFileFailed { get; set; }\n\n    /// <summary>Fires when a file is removed from the list.</summary>\n    [Parameter] public EventCallback<FileUploadItem> OnFileRemoved { get; set; }\n\n    /// <summary>Fires when a file is rejected due to type/size/count constraints.</summary>\n    [Parameter] public EventCallback<(IBrowserFile File, string Reason)> OnFileRejected { get; set; }\n\n    // ── CSS / additional attrs ───────────────────────────────────────────────\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    // ── Enums ────────────────────────────────────────────────────────────────\n    public enum FileUploadVariant { Dropzone, Button, Avatar }\n    public enum FileUploadSize { Sm, Default, Lg }\n\n    /// <summary>Per-file upload state tracked internally and exposed via events.</summary>\n    public class FileUploadItem\n    {\n        public string Id { get; init; } = Guid.NewGuid().ToString(\"N\");\n        public IBrowserFile File { get; init; } = default!;\n        public string Name { get; init; } = string.Empty;\n        public long Size { get; init; }\n        public FileUploadStatus Status { get; set; } = FileUploadStatus.Pending;\n        public int ProgressPercent { get; set; }\n        public string? ErrorMessage { get; set; }\n        /// <summary>Server-returned URL or id after a successful upload.</summary>\n        public string? Url { get; set; }\n        /// <summary>Chunk-size hint (bytes) from <see cref=\"FileUpload.ChunkSize\"/>. Zero if not configured.</summary>\n        public long ChunkSize { get; init; }\n    }\n\n    public enum FileUploadStatus { Pending, Uploading, Succeeded, Failed, Cancelled }\n\n    // ── Internal state ───────────────────────────────────────────────────────\n    private bool _isDragOver;\n    private string? _globalErrorMessage;\n    private string? _thumbnailUrl;\n    private List<FileUploadItem> _uploadItems = new();\n    private Dictionary<string, string> _thumbnailUrls = new();\n    private readonly Dictionary<string, CancellationTokenSource> _cancellations = new();\n    private SemaphoreSlim? _concurrencySemaphore;\n    private int _currentBatchId;\n\n    // Kept for back-compat so existing code that reads _selectedFiles-equivalent still compiles.\n    // Exposed as a convenience projection.\n    private List<IBrowserFile> _selectedFiles => _uploadItems.Select(i => i.File).ToList();\n\n    // ── CSS helpers ──────────────────────────────────────────────────────────\n    private string ButtonSizeClasses => Size switch\n    {\n        FileUploadSize.Sm => \"h-8 px-3 text-xs\",\n        FileUploadSize.Lg => \"h-10 px-8 text-sm\",\n        _ => \"h-9 px-4 py-2 text-sm\"\n    };\n\n    private string ButtonLabelCssClass =>\n        $\"inline-flex cursor-pointer items-center justify-center gap-2 rounded-md bg-primary {ButtonSizeClasses} font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50\";\n\n    private string DropzoneCssClass => $\"flex cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed transition-colors {(_isDragOver ? \"border-primary bg-primary/5\" : EffectiveInvalid ? \"border-destructive bg-destructive/5\" : _globalErrorMessage is not null ? \"border-destructive bg-destructive/5\" : \"border-input bg-background hover:border-primary hover:bg-muted\")} px-6\".Trim();\n\n    // ── Drag handlers ────────────────────────────────────────────────────────\n    private void HandleDragEnter(DragEventArgs e) => _isDragOver = true;\n    private void HandleDragLeave(DragEventArgs e) => _isDragOver = false;\n    private async Task HandleDrop(DragEventArgs e) => _isDragOver = false;\n\n    // ── File selected ────────────────────────────────────────────────────────\n    private async Task HandleFileSelected(InputFileChangeEventArgs e)\n    {\n        _globalErrorMessage = null;\n\n        var candidates = Multiple ? e.GetMultipleFiles(MaxFiles + 1).ToList() : new List<IBrowserFile> { e.File };\n\n        // Count check against existing non-failed items\n        var existing = _uploadItems.Count(i => i.Status != FileUploadStatus.Failed && i.Status != FileUploadStatus.Cancelled);\n        if (existing + candidates.Count > MaxFiles)\n        {\n            var reason = MaxFilesError ?? $\"Maximum {MaxFiles} file{(MaxFiles != 1 ? \"s\" : \"\")} allowed.\";\n            _globalErrorMessage = reason;\n            // Still add them as rejected items\n            foreach (var f in candidates)\n            {\n                var rejected = new FileUploadItem { File = f, Name = f.Name, Size = f.Size, Status = FileUploadStatus.Failed, ErrorMessage = reason, ChunkSize = ChunkSize ?? 0 };\n                _uploadItems.Add(rejected);\n                await OnFileRejected.InvokeAsync((f, reason));\n            }\n            await OnFilesSelected.InvokeAsync(e);\n            return;\n        }\n\n        var validFiles = new List<IBrowserFile>();\n\n        foreach (var file in candidates)\n        {\n            // Size validation\n            if (file.Size > MaxFileSize)\n            {\n                var reason = MaxFileSizeError ?? L[\"FileUpload.ExceedsMaxSize\", file.Name, FormatSize(MaxFileSize)];\n                var rejected = new FileUploadItem { File = file, Name = file.Name, Size = file.Size, Status = FileUploadStatus.Failed, ErrorMessage = reason, ChunkSize = ChunkSize ?? 0 };\n                _uploadItems.Add(rejected);\n                await OnFileRejected.InvokeAsync((file, reason));\n                continue;\n            }\n\n            // Accept/type validation\n            if (!string.IsNullOrWhiteSpace(Accept) && !IsAccepted(file, Accept))\n            {\n                var reason = FileTypeError ?? $\"File \\\"{file.Name}\\\" has an unsupported file type.\";\n                var rejected = new FileUploadItem { File = file, Name = file.Name, Size = file.Size, Status = FileUploadStatus.Failed, ErrorMessage = reason, ChunkSize = ChunkSize ?? 0 };\n                _uploadItems.Add(rejected);\n                await OnFileRejected.InvokeAsync((file, reason));\n                continue;\n            }\n\n            validFiles.Add(file);\n            var item = new FileUploadItem { File = file, Name = file.Name, Size = file.Size, Status = FileUploadStatus.Pending, ChunkSize = ChunkSize ?? 0 };\n            _uploadItems.Add(item);\n        }\n\n        // Thumbnails for avatar variant\n        if (ShowThumbnails && Variant == FileUploadVariant.Avatar && validFiles.Count > 0 && IsImageFile(validFiles[0]))\n        {\n            try\n            {\n                var stream = validFiles[0].OpenReadStream(MaxFileSize);\n                using var ms = new MemoryStream();\n                await stream.CopyToAsync(ms);\n                _thumbnailUrl = $\"data:{validFiles[0].ContentType};base64,{Convert.ToBase64String(ms.ToArray())}\";\n            }\n            catch { _thumbnailUrl = null; }\n        }\n\n        // Thumbnails for dropzone variant\n        if (ShowThumbnails && Variant == FileUploadVariant.Dropzone)\n        {\n            foreach (var file in validFiles.Where(IsImageFile))\n            {\n                try\n                {\n                    var stream = file.OpenReadStream(MaxFileSize);\n                    using var ms = new MemoryStream();\n                    await stream.CopyToAsync(ms);\n                    _thumbnailUrls[file.Name] = $\"data:{file.ContentType};base64,{Convert.ToBase64String(ms.ToArray())}\";\n                }\n                catch { /* skip failed thumbnails */ }\n            }\n        }\n\n        await OnFilesSelected.InvokeAsync(e);\n\n        if (OnUpload is not null && AutoUpload && validFiles.Count > 0)\n        {\n            var pendingItems = _uploadItems.Where(i => i.Status == FileUploadStatus.Pending && validFiles.Contains(i.File)).ToList();\n            _ = Task.Run(() => RunUploadsAsync(pendingItems));\n        }\n    }\n\n    // ── Public: start uploads manually (when AutoUpload=false) ───────────────\n    public async Task StartUploadAsync()\n    {\n        if (OnUpload is null) return;\n        var pending = _uploadItems.Where(i => i.Status == FileUploadStatus.Pending).ToList();\n        if (pending.Count == 0) return;\n        await RunUploadsAsync(pending);\n    }\n\n    // ── Retry a single failed item ───────────────────────────────────────────\n    private async Task RetryItemAsync(FileUploadItem item)\n    {\n        if (OnUpload is null) return;\n        item.Status = FileUploadStatus.Pending;\n        item.ErrorMessage = null;\n        item.ProgressPercent = 0;\n        await InvokeAsync(StateHasChanged);\n        await RunUploadsAsync(new[] { item });\n    }\n\n    // ── Remove / cancel item ─────────────────────────────────────────────────\n    private async Task RemoveItemAsync(FileUploadItem item)\n    {\n        if (item.Status == FileUploadStatus.Uploading)\n        {\n            if (_cancellations.TryGetValue(item.Id, out var cts))\n            {\n                cts.Cancel();\n                _cancellations.Remove(item.Id);\n            }\n        }\n        _uploadItems.Remove(item);\n        _thumbnailUrls.Remove(item.Name);\n        await OnFileRemoved.InvokeAsync(item);\n    }\n\n    private async Task CancelItemAsync(FileUploadItem item)\n    {\n        if (_cancellations.TryGetValue(item.Id, out var cts))\n        {\n            cts.Cancel();\n            _cancellations.Remove(item.Id);\n        }\n        item.Status = FileUploadStatus.Cancelled;\n        await InvokeAsync(StateHasChanged);\n    }\n\n    // ── Upload orchestrator ───────────────────────────────────────────────────\n    private async Task RunUploadsAsync(IEnumerable<FileUploadItem> items)\n    {\n        _concurrencySemaphore ??= new SemaphoreSlim(MaxConcurrentUploads, MaxConcurrentUploads);\n\n        // Capture this batch's id so a superseding batch can prevent stale callbacks.\n        var batchId = ++_currentBatchId;\n\n        var tasks = items.Select(item => UploadOneAsync(item)).ToList();\n        await Task.WhenAll(tasks);\n\n        // If a newer batch has started, don't fire the callback with stale results.\n        if (batchId != _currentBatchId) return;\n\n        // Check if all items are now in a terminal state\n        if (_uploadItems.All(i => i.Status is FileUploadStatus.Succeeded or FileUploadStatus.Failed or FileUploadStatus.Cancelled))\n        {\n            await InvokeAsync(async () =>\n            {\n                await OnAllUploaded.InvokeAsync(_uploadItems.AsReadOnly());\n                StateHasChanged();\n            });\n        }\n    }\n\n    private async Task UploadOneAsync(FileUploadItem item)\n    {\n        await _concurrencySemaphore!.WaitAsync();\n        try\n        {\n            var cts = new CancellationTokenSource();\n            _cancellations[item.Id] = cts;\n\n            item.Status = FileUploadStatus.Uploading;\n            item.ProgressPercent = 0;\n            await InvokeAsync(StateHasChanged);\n\n            var progress = new Progress<int>(pct =>\n            {\n                item.ProgressPercent = Math.Clamp(pct, 0, 100);\n                InvokeAsync(StateHasChanged);\n            });\n\n            try\n            {\n                var url = await OnUpload!(item, progress, cts.Token);\n                if (cts.Token.IsCancellationRequested)\n                {\n                    item.Status = FileUploadStatus.Cancelled;\n                }\n                else\n                {\n                    item.Status = FileUploadStatus.Succeeded;\n                    item.ProgressPercent = 100;\n                    item.Url = url;\n                    await InvokeAsync(async () =>\n                    {\n                        await OnFileUploaded.InvokeAsync(item);\n                        StateHasChanged();\n                    });\n                }\n            }\n            catch (OperationCanceledException)\n            {\n                item.Status = FileUploadStatus.Cancelled;\n            }\n            catch (Exception ex)\n            {\n                item.Status = FileUploadStatus.Failed;\n                item.ErrorMessage = ex.Message;\n                await InvokeAsync(async () =>\n                {\n                    await OnFileFailed.InvokeAsync(item);\n                    StateHasChanged();\n                });\n            }\n            finally\n            {\n                _cancellations.Remove(item.Id);\n                try { cts.Dispose(); } catch (ObjectDisposedException) { }\n            }\n        }\n        finally\n        {\n            _concurrencySemaphore!.Release();\n            await InvokeAsync(StateHasChanged);\n        }\n    }\n\n    // ── Dispose ──────────────────────────────────────────────────────────────\n    public async ValueTask DisposeAsync()\n    {\n        // Cancel + dispose all in-flight upload tokens. Without this, every upload\n        // that was running when the component unmounted leaks its CancellationTokenSource.\n        foreach (var cts in _cancellations.Values.ToList())\n        {\n            try { cts.Cancel(); } catch (ObjectDisposedException) { }\n            cts.Dispose();\n        }\n        _cancellations.Clear();\n        await Task.CompletedTask;\n    }\n\n    // ── Per-file row renderer ────────────────────────────────────────────────\n    private RenderFragment RenderFileItem(FileUploadItem item) => builder =>\n    {\n        // Outer row\n        builder.OpenElement(0, \"div\");\n        builder.AddAttribute(1, \"class\", \"flex flex-col gap-1.5 rounded-md border border-border/40 bg-muted/30 p-2\");\n\n        // Top row: icon + name + size + action buttons\n        builder.OpenElement(2, \"div\");\n        builder.AddAttribute(3, \"class\", \"flex items-center gap-2\");\n\n        // File icon or thumbnail\n        if (ShowThumbnails && IsImageFile(item.File) && _thumbnailUrls.TryGetValue(item.Name, out var thumb))\n        {\n            builder.OpenElement(4, \"div\");\n            builder.AddAttribute(5, \"class\", \"h-8 w-8 shrink-0 overflow-hidden rounded bg-muted\");\n            builder.OpenElement(6, \"img\");\n            builder.AddAttribute(7, \"src\", thumb);\n            builder.AddAttribute(8, \"alt\", item.Name);\n            builder.AddAttribute(9, \"class\", \"h-full w-full object-cover\");\n            builder.CloseElement(); // img\n            builder.CloseElement(); // div\n        }\n        else\n        {\n            builder.OpenComponent<Blazicon>(10);\n            builder.AddAttribute(11, \"Svg\", Lucide.File);\n            builder.AddAttribute(12, \"class\", item.Status == FileUploadStatus.Failed ? \"h-4 w-4 shrink-0 text-destructive\" : \"h-4 w-4 shrink-0 text-muted-foreground\");\n            builder.CloseComponent();\n        }\n\n        // Name + size\n        builder.OpenElement(13, \"div\");\n        builder.AddAttribute(14, \"class\", \"flex-1 min-w-0\");\n        builder.OpenElement(15, \"p\");\n        builder.AddAttribute(16, \"class\", \"text-sm font-medium truncate\");\n        builder.AddContent(17, item.Name);\n        builder.CloseElement(); // p\n        builder.OpenElement(18, \"p\");\n        builder.AddAttribute(19, \"class\", \"text-xs text-muted-foreground\");\n        builder.AddContent(20, FormatSize(item.Size));\n        builder.CloseElement(); // p\n        builder.CloseElement(); // div name+size\n\n        // Status icon\n        if (item.Status == FileUploadStatus.Succeeded)\n        {\n            builder.OpenComponent<Blazicon>(21);\n            builder.AddAttribute(22, \"Svg\", Lucide.Check);\n            builder.AddAttribute(23, \"class\", \"h-4 w-4 shrink-0 text-success\");\n            builder.CloseComponent();\n        }\n        else if (item.Status == FileUploadStatus.Failed)\n        {\n            builder.OpenComponent<Blazicon>(24);\n            builder.AddAttribute(25, \"Svg\", Lucide.CircleX);\n            builder.AddAttribute(26, \"class\", \"h-4 w-4 shrink-0 text-destructive\");\n            builder.CloseComponent();\n        }\n\n        // Action buttons\n        if (item.Status == FileUploadStatus.Uploading)\n        {\n            // Cancel button\n            builder.OpenElement(27, \"button\");\n            builder.AddAttribute(28, \"type\", \"button\");\n            builder.AddAttribute(29, \"class\", \"inline-flex items-center justify-center h-6 w-6 rounded text-muted-foreground hover:text-destructive transition-colors\");\n            builder.AddAttribute(30, \"title\", \"Cancel\");\n            builder.AddAttribute(31, \"onclick\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create(this, () => CancelItemAsync(item)));\n            builder.OpenComponent<Blazicon>(32);\n            builder.AddAttribute(33, \"Svg\", Lucide.X);\n            builder.AddAttribute(34, \"class\", \"h-3.5 w-3.5\");\n            builder.CloseComponent();\n            builder.CloseElement(); // button\n        }\n        else if (item.Status == FileUploadStatus.Failed && OnUpload is not null && item.ErrorMessage?.Contains(\"exceeds\") == false && item.ErrorMessage?.Contains(\"unsupported\") == false)\n        {\n            // Retry button (only for upload failures, not validation rejections)\n            builder.OpenElement(35, \"button\");\n            builder.AddAttribute(36, \"type\", \"button\");\n            builder.AddAttribute(37, \"class\", \"inline-flex items-center justify-center h-6 w-6 rounded text-muted-foreground hover:text-primary transition-colors\");\n            builder.AddAttribute(38, \"title\", \"Retry\");\n            builder.AddAttribute(39, \"onclick\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create(this, () => RetryItemAsync(item)));\n            builder.OpenComponent<Blazicon>(40);\n            builder.AddAttribute(41, \"Svg\", Lucide.RotateCcw);\n            builder.AddAttribute(42, \"class\", \"h-3.5 w-3.5\");\n            builder.CloseComponent();\n            builder.CloseElement(); // button\n        }\n        else if (item.Status != FileUploadStatus.Uploading)\n        {\n            // Remove button\n            builder.OpenElement(43, \"button\");\n            builder.AddAttribute(44, \"type\", \"button\");\n            builder.AddAttribute(45, \"class\", \"inline-flex items-center justify-center h-6 w-6 rounded text-muted-foreground hover:text-destructive transition-colors\");\n            builder.AddAttribute(46, \"title\", \"Remove\");\n            builder.AddAttribute(47, \"onclick\", Microsoft.AspNetCore.Components.EventCallback.Factory.Create(this, () => RemoveItemAsync(item)));\n            builder.OpenComponent<Blazicon>(48);\n            builder.AddAttribute(49, \"Svg\", Lucide.X);\n            builder.AddAttribute(50, \"class\", \"h-3.5 w-3.5\");\n            builder.CloseComponent();\n            builder.CloseElement(); // button\n        }\n\n        builder.CloseElement(); // div top row\n\n        // Progress bar (when uploading or showing progress for succeeded)\n        if (item.Status == FileUploadStatus.Uploading || (item.Status == FileUploadStatus.Succeeded && item.ProgressPercent > 0))\n        {\n            builder.OpenElement(51, \"div\");\n            builder.AddAttribute(52, \"class\", \"h-1.5 w-full overflow-hidden rounded-full bg-muted\");\n            builder.OpenElement(53, \"div\");\n            var pct = Math.Clamp(item.ProgressPercent, 0, 100);\n            var color = item.Status == FileUploadStatus.Succeeded ? \"bg-success\" : \"bg-primary\";\n            builder.AddAttribute(54, \"class\", $\"h-full rounded-full {color} transition-all duration-300\");\n            builder.AddAttribute(55, \"style\", $\"width: {pct}%\");\n            builder.CloseElement(); // inner div\n            builder.CloseElement(); // outer div\n        }\n\n        // Error message\n        if (item.Status == FileUploadStatus.Failed && item.ErrorMessage is not null)\n        {\n            builder.OpenElement(56, \"p\");\n            builder.AddAttribute(57, \"class\", \"text-xs text-destructive\");\n            builder.AddContent(58, item.ErrorMessage);\n            builder.CloseElement(); // p\n        }\n\n        builder.CloseElement(); // outer row div\n    };\n\n    // ── Helpers ──────────────────────────────────────────────────────────────\n    private bool IsImageFile(IBrowserFile file) =>\n        file.ContentType.StartsWith(\"image/\", StringComparison.OrdinalIgnoreCase);\n\n    private string? GetThumbnailUrl(IBrowserFile file) =>\n        _thumbnailUrls.TryGetValue(file.Name, out var url) ? url : null;\n\n    private static bool IsAccepted(IBrowserFile file, string accept)\n    {\n        var tokens = accept.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n        foreach (var token in tokens)\n        {\n            if (token.StartsWith('.'))\n            {\n                if (file.Name.EndsWith(token, StringComparison.OrdinalIgnoreCase)) return true;\n            }\n            else if (token.EndsWith(\"/*\"))\n            {\n                var prefix = token[..^1]; // e.g. \"image/\"\n                if (file.ContentType.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return true;\n            }\n            else\n            {\n                if (string.Equals(file.ContentType, token, StringComparison.OrdinalIgnoreCase)) return true;\n            }\n        }\n        return false;\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.0):F1} MB\";\n        return $\"{bytes / (1024.0 * 1024.0 * 1024.0):F1} GB\";\n    }\n}\n"
    }
  ],
  "slots": [
    {
      "name": "FileTemplate",
      "owner": "FileUpload",
      "type": "RenderFragment<IBrowserFile>?",
      "description": null
    }
  ],
  "serviceDependencies": [
    {
      "service": "ILumeoLocalizer",
      "namespace": "Lumeo.Services.Localization"
    }
  ],
  "cascadingDependencies": [
    {
      "name": "FormField",
      "type": "FormField.FormFieldContext?",
      "required": false
    }
  ],
  "relatedComponents": [],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Tests/Components/FileUpload/FileUploadTests.cs"
  ],
  "mdSummary": "# FileUpload\n\nDrag-and-drop file dropzone with progress and validation.\n\n**Package:** `Lumeo` · **Category:** Forms/Specialized\n**Docs:** https://lumeo.nativ.sh/components/file-upload\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| FileUpload | FormField | `FormField.FormFieldContext?` | `` |  |\n| FileUpload | Required | `bool` | `` |  |\n| FileUpload | Invalid | `bool` | `` |  |\n| FileUpload | ErrorText | `string?` | `` |  |\n| FileUpload | HelperText | `string?` | `` |  |\n| FileUpload | Name | `string?` | `` |  |\n| FileUpload | Label | `string?` | `` |  |\n| FileUpload | Description | `string?` | `` |  |\n| FileUpload | Multiple | `bool` | `` |  |\n| FileUpload | Accept | `string?` | `` |  |\n| FileUpload | MaxFileSize | `long` | `10 * 1024 * 1024` |  |\n| FileUpload | MaxFiles | `int` | `10` |  |\n| FileUpload | ShowThumbnails | `bool` | `` |  |\n| FileUpload | FileTemplate | `RenderFragment<IBrowserFile>?` | `` |  |\n| FileUpload | Variant | `FileUploadVariant` | `FileUploadVariant.Dropzone` |  |\n| FileUpload | Size | `FileUploadSize` | `FileUploadSize.Default` | Size of the trigger when Variant=\"Button\". Mirrors ButtonSize so a button-variant FileUpload can match neighbouring buttons in toolbars or compact forms. Has no visual effect on Dropzone or Avatar variants today. |\n| FileUpload | ShowProgress | `bool` | `` |  |\n| FileUpload | Progress | `int` | `` |  |\n| FileUpload | OnUpload | `Func<FileUploadItem, IProgress<int>, CancellationToken, Task<string?>>?` | `` | Consumer-supplied upload handler. Called once per file after selection (or after explicit StartUploadAsync if AutoUpload is false). Receives the item, an IProgress{T} for reporting percent 0-100, and a CancellationToken that fires if the user clicks Cancel. Should return the server-assigned URL/id on success, or throw/return null on failure. |\n| FileUpload | AutoUpload | `bool` | `true` | When true (default), uploads begin immediately after files are selected. When false, the user must click the Upload button to start. Requires OnUpload to be set. |\n| FileUpload | MaxConcurrentUploads | `int` | `3` | Maximum number of OnUpload calls running simultaneously. |\n| FileUpload | ChunkSize | `long?` | `` | Optional chunk size hint (bytes) for consumers that want to implement chunked/resumable upload inside their OnUpload handler. The component passes this value through ChunkSize but does NOT implement chunking itself. |\n| FileUpload | MaxFileSizeError | `string?` | `` |  |\n| FileUpload | MaxFilesError | `string?` | `` |  |\n| FileUpload | FileTypeError | `string?` | `` |  |\n| FileUpload | OnFilesSelected | `EventCallback<InputFileChangeEventArgs>` | `` | Fires when files are selected (raw args, kept for back-compat). |\n| FileUpload | OnFileUploaded | `EventCallback<FileUploadItem>` | `` | Fires each time a file upload succeeds. |\n| FileUpload | OnAllUploaded | `EventCallback<IReadOnlyList<FileUploadItem>>` | `` | Fires when all queued uploads have finished (succeeded, failed, or cancelled). |\n| FileUpload | OnFileFailed | `EventCallback<FileUploadItem>` | `` | Fires each time a file upload fails. |\n| FileUpload | OnFileRemoved | `EventCallback<FileUploadItem>` | `` | Fires when a file is removed from the list. |\n| FileUpload | OnFileRejected | `EventCallback<(IBrowserFile File, string Reason)>` | `` | Fires when a file is rejected due to type/size/count constraints. |\n| FileUpload | Class | `string?` | `` |  |\n| FileUpload | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- `FileTemplate` (FileUpload) — RenderFragment slot\n\n## Events\n- `OnFilesSelected` (`EventCallback<InputFileChangeEventArgs>`) — Fires when files are selected (raw args, kept for back-compat).\n- `OnFileUploaded` (`EventCallback<FileUploadItem>`) — Fires each time a file upload succeeds.\n- `OnAllUploaded` (`EventCallback<IReadOnlyList<FileUploadItem>>`) — Fires when all queued uploads have finished (succeeded, failed, or cancelled).\n- `OnFileFailed` (`EventCallback<FileUploadItem>`) — Fires each time a file upload fails.\n- `OnFileRemoved` (`EventCallback<FileUploadItem>`) — Fires when a file is removed from the list.\n- `OnFileRejected` (`EventCallback<(IBrowserFile File, string Reason)>`) — Fires when a file is rejected due to type/size/count constraints.\n\n## Sub-components\n- (none)\n\n## Cascading values it expects\n- `FormField` (`FormField.FormFieldContext?`)\n\n## Services it injects\n- `ILumeoLocalizer` (Lumeo.Services.Localization)\n\n## Keyboard\n—\n\n## Example\n```razor\n<FileUpload />\n```\n\n## CSS variables\n`--color-background`, `--color-border`, `--color-destructive`, `--color-input`, `--color-muted`, `--color-muted-foreground`, `--color-primary`, `--color-primary-foreground`, `--radius`\n\n## Files\n- `UI/FileUpload/FileUpload.razor`\n"
}