{
  "name": "Toast",
  "category": "Feedback",
  "subcategory": null,
  "description": "Notification toast — renders from ToastService queue.",
  "thumbnail": "/preview-cards/toast.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/Toast/Toast.razor",
    "UI/Toast/ToastAction.razor",
    "UI/Toast/ToastClose.razor",
    "UI/Toast/ToastDescription.razor",
    "UI/Toast/ToastProvider.razor",
    "UI/Toast/ToastProvider.razor.cs",
    "UI/Toast/ToastTitle.razor",
    "UI/Toast/ToastViewport.razor"
  ],
  "dependencies": [],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-border",
    "--color-card",
    "--color-destructive",
    "--color-foreground",
    "--color-primary",
    "--color-primary-foreground",
    "--color-ring",
    "--radius"
  ],
  "api": {
    "name": "Toast",
    "category": "Feedback",
    "subcategory": null,
    "description": "Notification toast — renders from ToastService queue.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/Toast/Toast.razor",
      "UI/Toast/ToastAction.razor",
      "UI/Toast/ToastClose.razor",
      "UI/Toast/ToastDescription.razor",
      "UI/Toast/ToastProvider.razor",
      "UI/Toast/ToastProvider.razor.cs",
      "UI/Toast/ToastTitle.razor",
      "UI/Toast/ToastViewport.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [],
    "parameters": [
      {
        "name": "ChildContent",
        "type": "RenderFragment?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Variant",
        "type": "ToastVariant",
        "default": "ToastVariant.Default",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Class",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "AdditionalAttributes",
        "type": "Dictionary<string, object>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": true
      }
    ],
    "events": [],
    "enums": [],
    "records": [],
    "cssVars": [
      "--color-border",
      "--color-card",
      "--color-destructive",
      "--color-foreground",
      "--color-primary",
      "--color-primary-foreground",
      "--color-ring",
      "--radius"
    ],
    "examples": [
      {
        "title": "Default Toast",
        "code": "@inject ToastService ToastService\n\n<Button OnClick=\"ShowDefault\">Show Toast</Button>\n\n<ToastProvider />\n\n@code {\n    private void ShowDefault()\n    {\n        ToastService.Show(\"Scheduled\", \"Your event has been created.\");\n    }\n}"
      },
      {
        "title": "Success Toast",
        "code": "ToastService.Success(\"Success\", \"Your changes have been saved.\");"
      },
      {
        "title": "Destructive Toast",
        "code": "ToastService.Error(\"Error\", \"Something went wrong. Please try again.\");"
      },
      {
        "title": "All Variants",
        "code": "@inject ToastService ToastService\n\n<Button OnClick=\"ShowDefault\">Default</Button>\n<Button OnClick=\"ShowSuccess\">Success</Button>\n<Button OnClick=\"ShowError\">Error</Button>\n\n<ToastProvider />\n\n@code {\n    private void ShowDefault()\n        => ToastService.Show(\"Scheduled\", \"Your event has been created.\");\n    private void ShowSuccess()\n        => ToastService.Success(\"Success\", \"Your changes have been saved.\");\n    private void ShowError()\n        => ToastService.Error(\"Error\", \"Something went wrong. Please try again.\");\n}"
      }
    ],
    "subComponents": {
      "ToastAction": {
        "componentName": "ToastAction",
        "fileName": "ToastAction.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [],
        "parameters": [
          {
            "name": "Label",
            "type": "string?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "OnClick",
            "type": "EventCallback",
            "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": "OnClick",
            "type": "EventCallback",
            "description": null
          }
        ],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      },
      "ToastClose": {
        "componentName": "ToastClose",
        "fileName": "ToastClose.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [],
        "parameters": [
          {
            "name": "OnClose",
            "type": "EventCallback",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "AriaLabel",
            "type": "string",
            "default": "\"Close\"",
            "description": "Accessible name for the close button. Override for i18n.",
            "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": "OnClose",
            "type": "EventCallback",
            "description": null
          }
        ],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      },
      "ToastDescription": {
        "componentName": "ToastDescription",
        "fileName": "ToastDescription.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [],
        "parameters": [
          {
            "name": "ChildContent",
            "type": "RenderFragment?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Class",
            "type": "string?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "AdditionalAttributes",
            "type": "Dictionary<string, object>?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": true
          }
        ],
        "events": [],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      },
      "ToastProvider": {
        "componentName": "ToastProvider",
        "fileName": "ToastProvider.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [
          "IAsyncDisposable"
        ],
        "parameters": [
          {
            "name": "Position",
            "type": "ToastViewport.ToastPosition",
            "default": "ToastViewport.ToastPosition.BottomRight",
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "DefaultDuration",
            "type": "int",
            "default": "5000",
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "MaxToasts",
            "type": "int",
            "default": "5",
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          }
        ],
        "events": [],
        "enums": [],
        "records": [
          {
            "name": "ToastItem",
            "signature": "ToastItem(string Id, ToastOptions Options, bool Leaving = false)",
            "description": null
          }
        ],
        "parseFailed": false,
        "parseError": null
      },
      "ToastTitle": {
        "componentName": "ToastTitle",
        "fileName": "ToastTitle.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [],
        "parameters": [
          {
            "name": "ChildContent",
            "type": "RenderFragment?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Class",
            "type": "string?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "AdditionalAttributes",
            "type": "Dictionary<string, object>?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": true
          }
        ],
        "events": [],
        "enums": [],
        "records": [],
        "parseFailed": false,
        "parseError": null
      },
      "ToastViewport": {
        "componentName": "ToastViewport",
        "fileName": "ToastViewport.razor",
        "namespace": "Lumeo",
        "inheritsFrom": null,
        "implements": [],
        "parameters": [
          {
            "name": "ChildContent",
            "type": "RenderFragment?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Class",
            "type": "string?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "Position",
            "type": "ToastPosition",
            "default": "ToastPosition.BottomRight",
            "description": null,
            "isCascading": false,
            "captureUnmatched": false
          },
          {
            "name": "AdditionalAttributes",
            "type": "Dictionary<string, object>?",
            "default": null,
            "description": null,
            "isCascading": false,
            "captureUnmatched": true
          }
        ],
        "events": [],
        "enums": [
          {
            "name": "ToastPosition",
            "values": [
              "TopLeft",
              "TopCenter",
              "TopRight",
              "BottomLeft",
              "BottomCenter",
              "BottomRight"
            ],
            "description": null
          }
        ],
        "records": [],
        "parseFailed": false,
        "parseError": null
      }
    },
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "toast",
  "docsUrl": "https://lumeo.nativ.sh/components/toast",
  "sourceUrl": [
    {
      "file": "UI/Toast/Toast.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Toast/Toast.razor"
    },
    {
      "file": "UI/Toast/ToastAction.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Toast/ToastAction.razor"
    },
    {
      "file": "UI/Toast/ToastClose.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Toast/ToastClose.razor"
    },
    {
      "file": "UI/Toast/ToastDescription.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Toast/ToastDescription.razor"
    },
    {
      "file": "UI/Toast/ToastProvider.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Toast/ToastProvider.razor"
    },
    {
      "file": "UI/Toast/ToastProvider.razor.cs",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Toast/ToastProvider.razor.cs"
    },
    {
      "file": "UI/Toast/ToastTitle.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Toast/ToastTitle.razor"
    },
    {
      "file": "UI/Toast/ToastViewport.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Toast/ToastViewport.razor"
    }
  ],
  "source": [
    {
      "path": "UI/Toast/Toast.razor",
      "content": "@namespace Lumeo\n\n<div class=\"@CssClass\" role=\"alert\" @attributes=\"AdditionalAttributes\">\n    @ChildContent\n</div>\n\n@code {\n    [Parameter] public RenderFragment? ChildContent { get; set; }\n    [Parameter] public ToastVariant Variant { get; set; } = ToastVariant.Default;\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private string CssClass => $\"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-border/50 p-4 pr-11 sm:pr-6 shadow-lg animate-toast-in {VariantClasses} {Class}\".Trim();\n\n    private string VariantClasses => Variant switch\n    {\n        ToastVariant.Default => \"bg-card text-foreground\",\n        ToastVariant.Success => \"border-success/50 bg-success-light text-success-text\",\n        ToastVariant.Destructive => \"border-destructive/50 bg-destructive-light text-destructive-text\",\n        ToastVariant.Warning => \"border-warning/50 bg-warning-light text-warning-text\",\n        ToastVariant.Info => \"border-info/50 bg-info-light text-info-text\",\n        _ => \"bg-card text-foreground\"\n    };\n}\n"
    },
    {
      "path": "UI/Toast/ToastAction.razor",
      "content": "@namespace Lumeo\n\n<button class=\"@CssClass\" @onclick=\"OnClick\" @attributes=\"AdditionalAttributes\">\n    @Label\n</button>\n\n@code {\n    [Parameter] public string? Label { get; set; }\n    [Parameter] public EventCallback OnClick { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private string CssClass => $\"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-transparent bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors hover:opacity-90 focus:outline-none focus:ring-1 focus:ring-ring {Class}\".Trim();\n}\n"
    },
    {
      "path": "UI/Toast/ToastClose.razor",
      "content": "@namespace Lumeo\n\n<button type=\"button\" class=\"@CssClass\" aria-label=\"@AriaLabel\" @onclick=\"OnClose\" @attributes=\"AdditionalAttributes\">\n    <Blazicon Svg=\"Lucide.X\" class=\"h-4 w-4\" />\n    <span class=\"sr-only\">@AriaLabel</span>\n</button>\n\n@code {\n    [Parameter] public EventCallback OnClose { get; set; }\n    /// <summary>Accessible name for the close button. Override for i18n.</summary>\n    [Parameter] public string AriaLabel { get; set; } = \"Close\";\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    // Position: 4px inset on mobile so the 44px focus ring doesn't clip the toast border.\n    // Size: 44px on touch (WCAG 2.5.5) → 24px on desktop with hover-reveal pattern.\n    // Affordance: text-foreground/50 → hover/focus text-foreground + subtle bg → clear interactive state on every variant.\n    // Focus ring: ring-current so it inherits text color, staying visible on coloured variants (Destructive, Success, Warning, Info).\n    private string CssClass => $\"group/close absolute right-1 top-1 inline-flex items-center justify-center rounded-md h-11 w-11 sm:right-1 sm:top-1 sm:h-6 sm:w-6 text-foreground/50 hover:text-foreground hover:bg-foreground/10 focus-visible:bg-foreground/10 focus-visible:text-foreground opacity-100 sm:opacity-0 transition-[opacity,color,background-color] sm:group-hover:opacity-100 focus-visible:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-current focus-visible:ring-offset-1 focus-visible:ring-offset-transparent {Class}\".Trim();\n}\n"
    },
    {
      "path": "UI/Toast/ToastDescription.razor",
      "content": "@namespace Lumeo\n\n<div class=\"@CssClass\" @attributes=\"AdditionalAttributes\">\n    @ChildContent\n</div>\n\n@code {\n    [Parameter] public RenderFragment? ChildContent { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private string CssClass => $\"text-xs opacity-90 {Class}\".Trim();\n}\n"
    },
    {
      "path": "UI/Toast/ToastProvider.razor",
      "content": "@namespace Lumeo\n@using Lumeo.Services\n@implements IAsyncDisposable\n\n@*  rc.42 — Per-toast Position routing.\n    Up to rc.41 we mounted a single <ToastViewport Position=\"Position\"> and every\n    toast landed there, silently ignoring ToastOptions.Position (the per-call\n    override added in rc.37). Now we route each toast to a viewport keyed by its\n    effective position (options.Position ?? provider Position). The provider's\n    own default Position viewport is ALWAYS rendered (even when empty) so\n    consumers that rely on the viewport DOM being present — for styling, layout\n    measurement, or test assertions on position classes — still see it. Extra\n    viewports are only spawned when a toast actually targets a different\n    position. *@\n@{\n    var positionsToRender = _toasts\n        .Select(t => ResolvePosition(t.Options.Position))\n        .Distinct()\n        .ToList();\n    if (!positionsToRender.Contains(Position))\n    {\n        positionsToRender.Insert(0, Position);\n    }\n}\n@foreach (var pos in positionsToRender)\n{\n    <ToastViewport Position=\"@pos\" @key=\"@pos\">\n        @foreach (var toast in _toasts.Where(t => ResolvePosition(t.Options.Position) == pos))\n        {\n            <Toast Variant=\"@toast.Options.Variant\" Class=\"@(toast.Leaving ? $\"animate-toast-out {toast.Options.Class}\" : toast.Options.Class)\" @key=\"@toast.Id\">\n                @if (toast.Options.CustomContent is not null)\n                {\n                    @toast.Options.CustomContent\n                }\n                else\n                {\n                    <div class=\"grid gap-1\">\n                        <ToastTitle>@toast.Options.Title</ToastTitle>\n                        @if (toast.Options.Description is not null)\n                        {\n                            <ToastDescription>@toast.Options.Description</ToastDescription>\n                        }\n                        @if (toast.Options.ActionLabel is not null)\n                        {\n                            <ToastAction OnClick=\"@(() => InvokeAction(toast))\" Label=\"@toast.Options.ActionLabel\" />\n                        }\n                    </div>\n                }\n                @if (toast.Options.Dismissible)\n                {\n                    <ToastClose OnClose=\"@(() => Remove(toast.Id))\" />\n                }\n            </Toast>\n        }\n    </ToastViewport>\n}\n\n\n@inject ToastService ToastService\n\n@code {\n    private readonly List<ToastItem> _toasts = new();\n    private readonly Dictionary<string, CancellationTokenSource> _timers = new();\n    private readonly Dictionary<string, CancellationTokenSource> _exitTimers = new();\n\n    [Parameter] public ToastViewport.ToastPosition Position { get; set; } = ToastViewport.ToastPosition.BottomRight;\n    [Parameter] public int DefaultDuration { get; set; } = 5000;\n    [Parameter] public int MaxToasts { get; set; } = 5;\n\n    protected override void OnInitialized()\n    {\n        ToastService.OnShow += HandleShow;\n        ToastService.OnDismiss += HandleDismiss;\n        ToastService.OnUpdate += HandleUpdate;\n    }\n\n    private void HandleShow(ToastMessage message)\n    {\n        SafeAsyncDispatcher.FireAndForget(InvokeAsync, () => HandleShowAsync(message), \"ToastProvider.HandleShow\");\n    }\n\n    private async Task HandleShowAsync(ToastMessage message)\n    {\n        var item = new ToastItem(message.Id, message.Options);\n\n        // Enforce MaxToasts: evict oldest non-persistent toasts\n        while (_toasts.Count >= MaxToasts)\n        {\n            var oldest = _toasts.FirstOrDefault(t => t.Options.Duration != 0);\n            if (oldest is null) break;\n            await RemoveItemAsync(oldest.Id);\n        }\n\n        _toasts.Add(item);\n        StateHasChanged();\n\n        var duration = message.Options.Duration ?? DefaultDuration;\n        if (duration > 0)\n        {\n            var cts = new CancellationTokenSource();\n            _timers[message.Id] = cts;\n            SafeAsyncDispatcher.FireAndForget(InvokeAsync, () => DismissAfterDelayAsync(message.Id, duration, cts.Token), \"ToastProvider.DismissAfterDelay\");\n        }\n    }\n\n    private void HandleDismiss(string toastId)\n    {\n        SafeAsyncDispatcher.FireAndForget(InvokeAsync, () => HandleDismissAsync(toastId), \"ToastProvider.HandleDismiss\");\n    }\n\n    private async Task HandleDismissAsync(string toastId)\n    {\n        if (toastId == \"*\")\n        {\n            var ids = _toasts.Select(t => t.Id).ToList();\n            await Task.WhenAll(ids.Select(id => RemoveItemAsync(id)));\n        }\n        else\n        {\n            await RemoveItemAsync(toastId);\n        }\n        StateHasChanged();\n    }\n\n    private void HandleUpdate(string toastId, ToastOptions options)\n    {\n        SafeAsyncDispatcher.FireAndForget(InvokeAsync, () => HandleUpdateAsync(toastId, options), \"ToastProvider.HandleUpdate\");\n    }\n\n    private async Task HandleUpdateAsync(string toastId, ToastOptions options)\n    {\n        var index = _toasts.FindIndex(t => t.Id == toastId);\n        if (index < 0) return;\n\n        var updated = new ToastItem(toastId, options);\n        _toasts[index] = updated;\n\n        // Reset timer\n        CancelTimer(toastId);\n        var duration = options.Duration ?? DefaultDuration;\n        if (duration > 0)\n        {\n            var cts = new CancellationTokenSource();\n            _timers[toastId] = cts;\n            SafeAsyncDispatcher.FireAndForget(InvokeAsync, () => DismissAfterDelayAsync(toastId, duration, cts.Token), \"ToastProvider.DismissAfterDelay\");\n        }\n\n        StateHasChanged();\n    }\n\n    private async Task DismissAfterDelayAsync(string id, int duration, CancellationToken token)\n    {\n        try\n        {\n            await Task.Delay(duration, token);\n            // Use the async variant so the list removal is followed by StateHasChanged —\n            // without it the toast was dropped from _toasts but never re-rendered away.\n            await InvokeAsync(() => RemoveItemAsync(id));\n        }\n        catch (TaskCanceledException)\n        {\n            // Timer was cancelled — toast was manually dismissed or updated\n        }\n    }\n\n    private void InvokeAction(ToastItem toast)\n    {\n        toast.Options.OnAction?.Invoke();\n    }\n\n    // Exit animation duration — must match `.animate-toast-out` in lumeo.css (220 ms).\n    private const int ExitAnimationMs = 220;\n\n    private void Remove(string id)\n    {\n        SafeAsyncDispatcher.FireAndForget(InvokeAsync, () => RemoveWithExitAsync(id, runOnDismiss: true), \"ToastProvider.Remove\");\n    }\n\n    private void RemoveItem(string id)\n    {\n        CancelTimer(id);\n        _toasts.RemoveAll(t => t.Id == id);\n    }\n\n    private async Task RemoveItemAsync(string id)\n    {\n        await RemoveWithExitAsync(id, runOnDismiss: false);\n    }\n\n    private async Task RemoveWithExitAsync(string id, bool runOnDismiss)\n    {\n        var toast = _toasts.FirstOrDefault(t => t.Id == id);\n        if (toast is null) return;\n\n        CancelTimer(id);\n        if (runOnDismiss)\n        {\n            toast.Options.OnDismiss?.Invoke();\n        }\n        // Swap in the leaving marker so the render loop adds the exit animation class,\n        // then drop the toast after the animation would have played.\n        var index = _toasts.IndexOf(toast);\n        if (index >= 0)\n        {\n            _toasts[index] = toast with { Leaving = true };\n            StateHasChanged();\n            var exitCts = new CancellationTokenSource();\n            _exitTimers[id] = exitCts;\n            try\n            {\n                await Task.Delay(ExitAnimationMs, exitCts.Token);\n            }\n            catch (OperationCanceledException) { }\n            finally\n            {\n                if (_exitTimers.TryGetValue(id, out var storedCts) && storedCts == exitCts)\n                {\n                    _exitTimers.Remove(id);\n                    exitCts.Dispose();\n                }\n            }\n        }\n        _toasts.RemoveAll(t => t.Id == id);\n        await InvokeAsync(StateHasChanged);\n    }\n\n    private void CancelTimer(string id)\n    {\n        if (_timers.TryGetValue(id, out var cts))\n        {\n            cts.Cancel();\n            cts.Dispose();\n            _timers.Remove(id);\n        }\n        if (_exitTimers.TryGetValue(id, out var exitCts))\n        {\n            exitCts.Cancel();\n            exitCts.Dispose();\n            _exitTimers.Remove(id);\n        }\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        ToastService.OnShow -= HandleShow;\n        ToastService.OnDismiss -= HandleDismiss;\n        ToastService.OnUpdate -= HandleUpdate;\n\n        foreach (var cts in _timers.Values)\n        {\n            cts.Cancel();\n            cts.Dispose();\n        }\n        _timers.Clear();\n\n        foreach (var cts in _exitTimers.Values)\n        {\n            cts.Cancel();\n            cts.Dispose();\n        }\n        _exitTimers.Clear();\n\n        await Task.CompletedTask;\n    }\n\n    private record ToastItem(string Id, ToastOptions Options, bool Leaving = false);\n\n    /// <summary>Resolves a per-toast <see cref=\"ToastPosition\"/> to the\n    /// <see cref=\"ToastViewport.ToastPosition\"/> the viewport component uses,\n    /// falling back to the provider's default <see cref=\"Position\"/> when the\n    /// toast didn't override. Two enums exist (one in\n    /// <c>Lumeo.Services</c> for the service-facing API, one nested in\n    /// <c>ToastViewport</c> for the visual component) — same members,\n    /// integer-cast bridges them. (rc.42)</summary>\n    private ToastViewport.ToastPosition ResolvePosition(ToastPosition? optionsPosition)\n        => optionsPosition is { } p ? (ToastViewport.ToastPosition)(int)p : Position;\n}\n"
    },
    {
      "path": "UI/Toast/ToastProvider.razor.cs",
      "content": "using Microsoft.AspNetCore.Components;\n\nnamespace Lumeo;\n\n/// <summary>\n/// Partial extension to <see cref=\"ToastProvider\"/> that adds the\n/// <see cref=\"MaxVisible\"/> stacking cap.\n/// When <see cref=\"MaxVisible\"/> &gt; 0 the provider syncs\n/// <see cref=\"MaxToasts\"/> so at most that many toasts are displayed;\n/// older toasts are evicted and resurface when newer ones are dismissed.\n/// </summary>\npublic partial class ToastProvider\n{\n    /// <summary>\n    /// Maximum number of toasts to display simultaneously. 0 = unlimited.\n    /// Older toasts beyond this cap are evicted (same behaviour as\n    /// <see cref=\"MaxToasts\"/>) and resurface when newer toasts are dismissed.\n    /// </summary>\n    [Parameter] public int MaxVisible { get; set; } = 0;\n\n    protected override void OnParametersSet()\n    {\n        base.OnParametersSet();\n        if (MaxVisible > 0)\n            MaxToasts = MaxVisible;\n    }\n}\n"
    },
    {
      "path": "UI/Toast/ToastTitle.razor",
      "content": "@namespace Lumeo\n\n<p class=\"@CssClass\" @attributes=\"AdditionalAttributes\">\n    @ChildContent\n</p>\n\n@code {\n    [Parameter] public RenderFragment? ChildContent { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private string CssClass => $\"text-sm font-semibold [&+div]:text-xs {Class}\".Trim();\n}\n"
    },
    {
      "path": "UI/Toast/ToastViewport.razor",
      "content": "@namespace Lumeo\n\n<div class=\"@CssClass\" @attributes=\"AdditionalAttributes\">\n    @ChildContent\n</div>\n\n@code {\n    [Parameter] public RenderFragment? ChildContent { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter] public ToastPosition Position { get; set; } = ToastPosition.BottomRight;\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    // On screens < sm (640px), every position collapses to a centered bottom strip with 16px side gutters\n    // so toasts can't get cut off or have their content (title + description + close button + action) squeezed\n    // to unreadable widths. From sm: up, the developer-chosen position takes effect.\n    // The mobile base is bottom-anchored because that's where one-thumb reach lives on a phone.\n    private string PositionClasses => Position switch\n    {\n        ToastPosition.TopLeft     => \"fixed inset-x-4 bottom-4 sm:bottom-auto sm:right-auto sm:inset-x-auto sm:top-4 sm:left-4\",\n        ToastPosition.TopCenter   => \"fixed inset-x-4 bottom-4 sm:bottom-auto sm:inset-x-auto sm:top-4 sm:left-1/2 sm:-translate-x-1/2\",\n        ToastPosition.TopRight    => \"fixed inset-x-4 bottom-4 sm:bottom-auto sm:inset-x-auto sm:top-4 sm:right-4\",\n        ToastPosition.BottomLeft  => \"fixed inset-x-4 bottom-4 sm:inset-x-auto sm:left-4\",\n        ToastPosition.BottomCenter => \"fixed inset-x-4 bottom-4 sm:inset-x-auto sm:left-1/2 sm:-translate-x-1/2\",\n        _ /* BottomRight */       => \"fixed inset-x-4 bottom-4 sm:inset-x-auto sm:right-4\",\n    };\n\n    // Mobile: full available width (constrained by inset-x-4 = 16px gutters each side).\n    // Desktop: capped at 420px so toasts don't stretch absurdly on wide monitors.\n    private string CssClass => $\"{PositionClasses} z-[100] flex max-h-screen w-auto sm:w-full sm:max-w-[420px] flex-col gap-2 {Class}\".Trim();\n\n    public enum ToastPosition { TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight }\n}\n"
    }
  ],
  "slots": [
    {
      "name": "ChildContent",
      "owner": "Toast",
      "type": "RenderFragment?",
      "description": null
    },
    {
      "name": "ChildContent",
      "owner": "ToastDescription",
      "type": "RenderFragment?",
      "description": null
    },
    {
      "name": "ChildContent",
      "owner": "ToastTitle",
      "type": "RenderFragment?",
      "description": null
    },
    {
      "name": "ChildContent",
      "owner": "ToastViewport",
      "type": "RenderFragment?",
      "description": null
    }
  ],
  "serviceDependencies": [
    {
      "service": "ToastService",
      "namespace": ""
    }
  ],
  "cascadingDependencies": [],
  "relatedComponents": [
    {
      "name": "ToastAction",
      "reason": "sub-component"
    },
    {
      "name": "ToastClose",
      "reason": "sub-component"
    },
    {
      "name": "ToastDescription",
      "reason": "sub-component"
    },
    {
      "name": "ToastProvider",
      "reason": "sub-component"
    },
    {
      "name": "ToastTitle",
      "reason": "sub-component"
    },
    {
      "name": "ToastViewport",
      "reason": "sub-component"
    }
  ],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Docs.Tests/Helpers/DocsTestContext.cs",
    "tests/Lumeo.Tests/Components/Toast/ToastPositioningTests.cs",
    "tests/Lumeo.Tests/Components/Toast/ToastTests.cs",
    "tests/Lumeo.Tests/Services/ComponentInteropServiceTests.cs",
    "tests/Lumeo.Tests/Services/Interop/InteropAdapterTests.cs",
    "tests/Lumeo.Tests/Services/StrictInteropTests.cs"
  ],
  "mdSummary": "# Toast\n\nNotification toast — renders from ToastService queue.\n\n**Package:** `Lumeo` · **Category:** Feedback\n**Docs:** https://lumeo.nativ.sh/components/toast\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| Toast | ChildContent | `RenderFragment?` | `` |  |\n| Toast | Variant | `ToastVariant` | `ToastVariant.Default` |  |\n| Toast | Class | `string?` | `` |  |\n| Toast | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| ToastAction | Label | `string?` | `` |  |\n| ToastAction | OnClick | `EventCallback` | `` |  |\n| ToastAction | Class | `string?` | `` |  |\n| ToastAction | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| ToastClose | OnClose | `EventCallback` | `` |  |\n| ToastClose | AriaLabel | `string` | `\"Close\"` | Accessible name for the close button. Override for i18n. |\n| ToastClose | Class | `string?` | `` |  |\n| ToastClose | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| ToastDescription | ChildContent | `RenderFragment?` | `` |  |\n| ToastDescription | Class | `string?` | `` |  |\n| ToastDescription | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| ToastProvider | Position | `ToastViewport.ToastPosition` | `ToastViewport.ToastPosition.BottomRight` |  |\n| ToastProvider | DefaultDuration | `int` | `5000` |  |\n| ToastProvider | MaxToasts | `int` | `5` |  |\n| ToastTitle | ChildContent | `RenderFragment?` | `` |  |\n| ToastTitle | Class | `string?` | `` |  |\n| ToastTitle | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| ToastViewport | ChildContent | `RenderFragment?` | `` |  |\n| ToastViewport | Class | `string?` | `` |  |\n| ToastViewport | Position | `ToastPosition` | `ToastPosition.BottomRight` |  |\n| ToastViewport | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- `ChildContent` (Toast) — RenderFragment slot\n- `ChildContent` (ToastDescription) — RenderFragment slot\n- `ChildContent` (ToastTitle) — RenderFragment slot\n- `ChildContent` (ToastViewport) — RenderFragment slot\n\n## Events\n- (none)\n\n## Sub-components\n- `ToastAction`, `ToastClose`, `ToastDescription`, `ToastProvider`, `ToastTitle`, `ToastViewport`\n\n## Cascading values it expects\n- (none)\n\n## Services it injects\n- `ToastService` ()\n\n## Keyboard\n—\n\n## Example\n```razor\n<Toast />\n```\n\n## CSS variables\n`--color-border`, `--color-card`, `--color-destructive`, `--color-foreground`, `--color-primary`, `--color-primary-foreground`, `--color-ring`, `--radius`\n\n## Files\n- `UI/Toast/Toast.razor`\n- `UI/Toast/ToastAction.razor`\n- `UI/Toast/ToastClose.razor`\n- `UI/Toast/ToastDescription.razor`\n- `UI/Toast/ToastProvider.razor`\n- `UI/Toast/ToastProvider.razor.cs`\n- `UI/Toast/ToastTitle.razor`\n- `UI/Toast/ToastViewport.razor`\n"
}