{
  "name": "Mention",
  "category": "Forms",
  "subcategory": "Selection",
  "description": "Textarea with @-trigger dropdown for mentioning users.",
  "thumbnail": "/preview-cards/mention.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/Mention/Mention.razor"
  ],
  "dependencies": [],
  "packageDependencies": [],
  "cssVars": [
    "--color-accent",
    "--color-accent-foreground",
    "--color-border",
    "--color-destructive",
    "--color-foreground",
    "--color-muted",
    "--color-muted-foreground",
    "--color-popover",
    "--color-ring",
    "--radius"
  ],
  "api": {
    "name": "Mention",
    "category": "Forms",
    "subcategory": "Selection",
    "description": "Textarea with @-trigger dropdown for mentioning users.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/Mention/Mention.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IAsyncDisposable"
    ],
    "parameters": [
      {
        "name": "FormField",
        "type": "FormField.FormFieldContext?",
        "default": null,
        "description": null,
        "isCascading": true,
        "captureUnmatched": false
      },
      {
        "name": "Value",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ValueChanged",
        "type": "EventCallback<string>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Trigger",
        "type": "char",
        "default": "'@'",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Options",
        "type": "List<MentionOption>",
        "default": "[]",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnSearch",
        "type": "EventCallback<string>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Disabled",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Placeholder",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Rows",
        "type": "int",
        "default": "3",
        "description": null,
        "isCascading": false,
        "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": "Label",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Name",
        "type": "string?",
        "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": "ValueChanged",
        "type": "EventCallback<string>",
        "description": null
      },
      {
        "name": "OnSearch",
        "type": "EventCallback<string>",
        "description": null
      }
    ],
    "enums": [],
    "records": [
      {
        "name": "MentionOption",
        "signature": "MentionOption(string Label, string Value, string? Avatar = null, string? Description = null)",
        "description": null
      }
    ],
    "cssVars": [
      "--color-accent",
      "--color-accent-foreground",
      "--color-border",
      "--color-destructive",
      "--color-foreground",
      "--color-muted",
      "--color-muted-foreground",
      "--color-popover",
      "--color-ring",
      "--radius"
    ],
    "examples": [
      {
        "title": "Basic Mention",
        "code": "<Mention @bind-Value=\"_text\" Options=\"@_people\" Placeholder=\"Type @@ to mention...\" />"
      },
      {
        "title": "Custom Trigger",
        "code": "<Mention @bind-Value=\"_text\" Options=\"@_channels\" Trigger='#' />"
      },
      {
        "title": "Trigger Character",
        "code": "<Segmented @bind-Value=\"_mentionTriggerStr\">\n    <SegmentedItem Value=\"@\">@@</SegmentedItem>\n    <SegmentedItem Value=\"#\">#</SegmentedItem>\n    <SegmentedItem Value=\"/\"> /</SegmentedItem>\n</Segmented>\n\n<Mention @bind-Value=\"_text\"\n         Options=\"@GetOptionsForTrigger(_mentionTriggerStr)\"\n         Trigger=\"@(_mentionTriggerStr[0])\"\n         Placeholder=\"@($\\\"Type {_mentionTriggerStr} to trigger suggestions...\\\")\" />\n\n@code {\n    private string _mentionTriggerStr = \"@\";\n    private string? _text;\n}"
      }
    ],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "mention",
  "docsUrl": "https://lumeo.nativ.sh/components/mention",
  "sourceUrl": [
    {
      "file": "UI/Mention/Mention.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Mention/Mention.razor"
    }
  ],
  "source": [
    {
      "path": "UI/Mention/Mention.razor",
      "content": "@namespace Lumeo\n@implements IAsyncDisposable\n\n@if (Label is not null && !InsideFormField)\n{\n    <label class=\"block text-sm font-medium text-foreground mb-1.5\">\n        @Label\n        @if (Required)\n        {\n            <span class=\"text-destructive ml-0.5\">*</span>\n        }\n    </label>\n}\n\n<div class=\"relative\" @attributes=\"AdditionalAttributes\">\n    <textarea id=\"@_textareaId\"\n              class=\"@TextareaCssClass\"\n              value=\"@Value\"\n              rows=\"@Rows\"\n              placeholder=\"@Placeholder\"\n              disabled=\"@Disabled\"\n              name=\"@(Name ?? FormField?.Name)\"\n              aria-required=\"@(Required ? \"true\" : \"false\")\"\n              aria-invalid=\"@(EffectiveInvalid ? \"true\" : \"false\")\"\n              @ref=\"_textareaRef\"\n              @oninput=\"HandleInput\"\n              @onkeydown=\"HandleKeyDown\"\n              @onfocus=\"HandleFocus\"\n              @onblur=\"HandleBlur\" />\n\n    @if (_showDropdown && FilteredOptions.Any())\n    {\n        <div id=\"@_dropdownId\" class=\"@DropdownCssClass\"\n             style=\"position: fixed; left: @(_caretLeft)px; top: @(_caretTop + 20)px;\">\n            @foreach (var (option, index) in FilteredOptions.Select((o, i) => (o, i)))\n            {\n                var isActive = index == _activeIndex;\n                <button @key=\"option.Value\" type=\"button\"\n                        class=\"@OptionCssClass(isActive)\"\n                        @onmousedown:preventDefault\n                        @onclick=\"() => SelectOption(option)\">\n                    @if (!string.IsNullOrEmpty(option.Avatar))\n                    {\n                        <img src=\"@option.Avatar\" alt=\"\" class=\"h-6 w-6 rounded-full\" />\n                    }\n                    <div class=\"flex flex-col\">\n                        <span class=\"text-sm font-medium text-foreground\">@option.Label</span>\n                        @if (!string.IsNullOrEmpty(option.Description))\n                        {\n                            <span class=\"text-xs text-muted-foreground\">@option.Description</span>\n                        }\n                    </div>\n                </button>\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 mt-1.5\">@ErrorText</p>\n    }\n    else if (HelperText is not null)\n    {\n        <p class=\"text-xs text-muted-foreground mt-1.5\">@HelperText</p>\n    }\n}\n\n@inject ComponentInteropService Interop\n\n@code {\n    [CascadingParameter] private FormField.FormFieldContext? FormField { get; set; }\n\n    [Parameter] public string? Value { get; set; }\n    [Parameter] public EventCallback<string> ValueChanged { get; set; }\n    [Parameter] public char Trigger { get; set; } = '@';\n    [Parameter] public List<MentionOption> Options { get; set; } = [];\n    [Parameter] public EventCallback<string> OnSearch { get; set; }\n    [Parameter] public bool Disabled { get; set; }\n    [Parameter] public string? Placeholder { get; set; }\n    [Parameter] public int Rows { get; set; } = 3;\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? Label { get; set; }\n    [Parameter] public string? Name { get; set; }\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private bool EffectiveInvalid => Invalid || FormField?.HasError == true;\n    private bool InsideFormField => FormField is not null;\n\n    private readonly string _textareaId = $\"mention-{Guid.NewGuid():N}\";\n    private readonly string _dropdownId = $\"mention-dd-{Guid.NewGuid():N}\";\n    private ElementReference _textareaRef;\n    private bool _showDropdown;\n    private string _searchTerm = \"\";\n    private int _triggerStartIndex = -1;\n    private int _activeIndex;\n    private double _caretTop;\n    private double _caretLeft;\n    private bool _hasFocus;\n    private CancellationTokenSource? _blurDelayCts;\n\n    private string TextareaCssClass => $\"flex min-h-[60px] w-full rounded-md border {(EffectiveInvalid ? \"border-destructive focus-visible:ring-destructive/20\" : \"border-border/60 focus-visible:ring-ring\")} bg-transparent px-3 py-2 text-sm text-foreground shadow-xs placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 {Class}\".Trim();\n    private string DropdownCssClass => \"z-50 min-w-[200px] max-h-[200px] overflow-auto rounded-md border border-border/60 bg-popover p-1 shadow-lg animate-fade-in\";\n\n    private static string OptionCssClass(bool isActive) =>\n        $\"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left outline-none transition-colors {(isActive ? \"bg-accent text-accent-foreground\" : \"hover:bg-accent hover:text-accent-foreground\")}\";\n\n    private List<MentionOption> FilteredOptions =>\n        string.IsNullOrEmpty(_searchTerm)\n            ? Options\n            : Options.Where(o => o.Label.Contains(_searchTerm, StringComparison.OrdinalIgnoreCase) ||\n                                 o.Value.Contains(_searchTerm, StringComparison.OrdinalIgnoreCase)).ToList();\n\n    private async Task HandleInput(ChangeEventArgs e)\n    {\n        var newValue = e.Value?.ToString() ?? \"\";\n        Value = newValue;\n        await ValueChanged.InvokeAsync(newValue);\n\n        // Get caret position from JS\n        var caretInfo = await Interop.GetTextareaCaretPosition(_textareaId);\n        if (caretInfo is null) return;\n        var selectionStart = caretInfo.SelectionStart;\n\n        // Check if we are in a mention context\n        var textBeforeCaret = selectionStart <= newValue.Length ? newValue[..selectionStart] : newValue;\n        var triggerIndex = textBeforeCaret.LastIndexOf(Trigger);\n\n        if (triggerIndex >= 0)\n        {\n            // Ensure trigger is at start or preceded by whitespace\n            var isValidTrigger = triggerIndex == 0 || char.IsWhiteSpace(textBeforeCaret[triggerIndex - 1]);\n            var textAfterTrigger = textBeforeCaret[(triggerIndex + 1)..];\n            var hasSpace = textAfterTrigger.Contains(' ');\n\n            if (isValidTrigger && !hasSpace)\n            {\n                _searchTerm = textAfterTrigger;\n                _triggerStartIndex = triggerIndex;\n                _showDropdown = true;\n                _activeIndex = 0;\n                _caretTop = caretInfo.Top;\n                _caretLeft = caretInfo.Left;\n\n                if (OnSearch.HasDelegate)\n                {\n                    await OnSearch.InvokeAsync(_searchTerm);\n                }\n                return;\n            }\n        }\n\n        _showDropdown = false;\n        _searchTerm = \"\";\n        _triggerStartIndex = -1;\n    }\n\n    private async Task HandleKeyDown(KeyboardEventArgs e)\n    {\n        if (!_showDropdown) return;\n\n        var filtered = FilteredOptions;\n        switch (e.Key)\n        {\n            case \"ArrowDown\":\n                _activeIndex = (_activeIndex + 1) % filtered.Count;\n                break;\n            case \"ArrowUp\":\n                _activeIndex = (_activeIndex - 1 + filtered.Count) % filtered.Count;\n                break;\n            case \"Enter\" or \"Tab\":\n                if (filtered.Count > 0 && _activeIndex < filtered.Count)\n                {\n                    await SelectOption(filtered[_activeIndex]);\n                }\n                break;\n            case \"Escape\":\n                _showDropdown = false;\n                // Return focus to the textarea so screen readers announce the correct context.\n                try { await _textareaRef.FocusAsync(); } catch { }\n                break;\n        }\n    }\n\n    private async Task SelectOption(MentionOption option)\n    {\n        if (Value is not null && _triggerStartIndex >= 0)\n        {\n            var caretInfo = await Interop.GetTextareaCaretPosition(_textareaId);\n            if (caretInfo is null) return;\n            var selectionStart = caretInfo.SelectionStart;\n            var before = Value[.._triggerStartIndex];\n            var after = selectionStart <= Value.Length ? Value[selectionStart..] : \"\";\n            Value = $\"{before}{Trigger}{option.Value} {after}\";\n            await ValueChanged.InvokeAsync(Value);\n        }\n\n        _showDropdown = false;\n        _searchTerm = \"\";\n        _triggerStartIndex = -1;\n        _activeIndex = 0;\n    }\n\n    private void HandleFocus()\n    {\n        _hasFocus = true;\n        // Cancel any pending blur-hide so re-focusing within the 150 ms window\n        // does not close the dropdown.\n        _blurDelayCts?.Cancel();\n    }\n\n    private async Task HandleBlur()\n    {\n        _hasFocus = false;\n        // Cancel any previous blur delay and start a fresh one.\n        _blurDelayCts?.Cancel();\n        _blurDelayCts?.Dispose();\n        _blurDelayCts = new CancellationTokenSource();\n        var token = _blurDelayCts.Token;\n        // Small delay to allow click on dropdown item\n        try { await Task.Delay(150, token); }\n        catch (TaskCanceledException) { return; }\n        if (_hasFocus) return; // re-focused during the wait\n        _showDropdown = false;\n        StateHasChanged();\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        _blurDelayCts?.Cancel();\n        _blurDelayCts?.Dispose();\n        _blurDelayCts = null;\n        await Task.CompletedTask;\n    }\n\n    public record MentionOption(string Label, string Value, string? Avatar = null, string? Description = null);\n}\n"
    }
  ],
  "slots": [],
  "serviceDependencies": [
    {
      "service": "ComponentInteropService",
      "namespace": ""
    }
  ],
  "cascadingDependencies": [
    {
      "name": "FormField",
      "type": "FormField.FormFieldContext?",
      "required": false
    }
  ],
  "relatedComponents": [],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Docs.Tests/Helpers/DocsTestContext.cs",
    "tests/Lumeo.Tests/Components/Mention/MentionTests.cs"
  ],
  "mdSummary": "# Mention\n\nTextarea with @-trigger dropdown for mentioning users.\n\n**Package:** `Lumeo` · **Category:** Forms/Selection\n**Docs:** https://lumeo.nativ.sh/components/mention\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| Mention | FormField | `FormField.FormFieldContext?` | `` |  |\n| Mention | Value | `string?` | `` |  |\n| Mention | ValueChanged | `EventCallback<string>` | `` |  |\n| Mention | Trigger | `char` | `'@'` |  |\n| Mention | Options | `List<MentionOption>` | `[]` |  |\n| Mention | OnSearch | `EventCallback<string>` | `` |  |\n| Mention | Disabled | `bool` | `` |  |\n| Mention | Placeholder | `string?` | `` |  |\n| Mention | Rows | `int` | `3` |  |\n| Mention | Required | `bool` | `` |  |\n| Mention | Invalid | `bool` | `` |  |\n| Mention | ErrorText | `string?` | `` |  |\n| Mention | HelperText | `string?` | `` |  |\n| Mention | Label | `string?` | `` |  |\n| Mention | Name | `string?` | `` |  |\n| Mention | Class | `string?` | `` |  |\n| Mention | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- (none)\n\n## Events\n- `ValueChanged` (`EventCallback<string>`)\n- `OnSearch` (`EventCallback<string>`)\n\n## Sub-components\n- (none)\n\n## Cascading values it expects\n- `FormField` (`FormField.FormFieldContext?`)\n\n## Services it injects\n- `ComponentInteropService` ()\n\n## Keyboard\n—\n\n## Example\n```razor\n<Mention />\n```\n\n## CSS variables\n`--color-accent`, `--color-accent-foreground`, `--color-border`, `--color-destructive`, `--color-foreground`, `--color-muted`, `--color-muted-foreground`, `--color-popover`, `--color-ring`, `--radius`\n\n## Files\n- `UI/Mention/Mention.razor`\n"
}