{
  "name": "OtpInput",
  "category": "Forms",
  "subcategory": "Inputs",
  "description": "One-time password input, auto-advances between boxes.",
  "thumbnail": "/preview-cards/otp-input.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/OtpInput/OtpInput.razor"
  ],
  "dependencies": [
    "list"
  ],
  "packageDependencies": [],
  "cssVars": [
    "--color-destructive",
    "--color-input",
    "--color-muted",
    "--color-muted-foreground",
    "--radius"
  ],
  "api": {
    "name": "OtpInput",
    "category": "Forms",
    "subcategory": "Inputs",
    "description": "One-time password input, auto-advances between boxes.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/OtpInput/OtpInput.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IAsyncDisposable"
    ],
    "parameters": [
      {
        "name": "FormField",
        "type": "FormField.FormFieldContext?",
        "default": null,
        "description": null,
        "isCascading": true,
        "captureUnmatched": false
      },
      {
        "name": "Length",
        "type": "int",
        "default": "6",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Value",
        "type": "string",
        "default": "\"\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ValueChanged",
        "type": "EventCallback<string>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnComplete",
        "type": "EventCallback<string>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "InputMode",
        "type": "string",
        "default": "\"Numeric\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Disabled",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Masked",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Separator",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "GroupSizes",
        "type": "int[]?",
        "default": null,
        "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": "OnComplete",
        "type": "EventCallback<string>",
        "description": null
      }
    ],
    "enums": [],
    "records": [],
    "cssVars": [
      "--color-destructive",
      "--color-input",
      "--color-muted",
      "--color-muted-foreground",
      "--radius"
    ],
    "examples": [
      {
        "title": "Default (6 digits)",
        "code": "<OtpInput @bind-Value=\"_otpValue\" OnComplete=\"HandleComplete\" />"
      },
      {
        "title": "2FA Verification",
        "code": "<Card>\n    <CardHeader>\n        <Heading Level=\"3\">Two-factor authentication</Heading>\n        <Lumeo.Text Size=\"sm\" Color=\"muted\">Enter the 6-digit code from your authenticator app.</Lumeo.Text>\n    </CardHeader>\n    <CardContent>\n        <OtpInput @bind-Value=\"_tfaValue\" OnComplete=\"HandleTfa\" />\n        @if (_tfaVerified)\n        {\n            <Badge Variant=\"Badge.BadgeVariant.Success\">Verified</Badge>\n        }\n    </CardContent>\n    <CardFooter>\n        <Button Class=\"w-full\" Disabled=\"@(_tfaValue.Length < 6)\">Verify</Button>\n    </CardFooter>\n</Card>"
      },
      {
        "title": "4 Digit Code",
        "code": "<OtpInput @bind-Value=\"_otp\" Length=\"4\" />"
      },
      {
        "title": "Variable Length",
        "code": "<Segmented @bind-Value=\"_otpLength\">\n    <SegmentedItem Value=\"4\">4 digits</SegmentedItem>\n    <SegmentedItem Value=\"6\">6 digits</SegmentedItem>\n    <SegmentedItem Value=\"8\">8 digits</SegmentedItem>\n</Segmented>\n\n<OtpInput Length=\"@(int.Parse(_otpLength))\" Value=\"@_otpValue\" ValueChanged=\"@(v => _otpValue = v)\" />\n\n@if (!string.IsNullOrEmpty(_otpValue))\n{\n    <Lumeo.Text Size=\"sm\" Color=\"muted\">Value: @_otpValue</Lumeo.Text>\n}\n\n@code {\n    private string _otpLength = \"6\";\n    private string _otpValue = \"\";\n}"
      }
    ],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "otp-input",
  "docsUrl": "https://lumeo.nativ.sh/components/otp-input",
  "sourceUrl": [
    {
      "file": "UI/OtpInput/OtpInput.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/OtpInput/OtpInput.razor"
    }
  ],
  "source": [
    {
      "path": "UI/OtpInput/OtpInput.razor",
      "content": "@namespace Lumeo\n\n@if (Label is not null && !InsideFormField)\n{\n    <label class=\"block text-sm font-medium 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=\"@CssClass\" @attributes=\"AdditionalAttributes\">\n    @{\n        var groups = GetGroups();\n        for (var g = 0; g < groups.Count; g++)\n        {\n            var group = groups[g];\n            <div class=\"flex items-center gap-2\">\n                @foreach (var idx in group)\n                {\n                    var index = idx;\n                    <input id=\"@($\"{_baseId}-{index}\")\"\n                           aria-required=\"@Required.ToString().ToLowerInvariant()\"\n                           aria-invalid=\"@EffectiveInvalid.ToString().ToLowerInvariant()\"\n                           type=\"@(Masked ? \"password\" : \"text\")\"\n                           inputmode=\"@InputModeAttr\"\n                           maxlength=\"1\"\n                           autocomplete=\"one-time-code\"\n                           class=\"@($\"flex h-10 w-10 items-center justify-center rounded-md border {(EffectiveInvalid ? \"border-destructive focus-visible:ring-[1.5px] focus-visible:ring-destructive/20\" : \"border-input\")} bg-transparent text-center text-sm font-medium transition-colors focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50\")\"\n                           disabled=\"@Disabled\"\n                           value=\"@GetChar(index)\"\n                           @oninput=\"@(e => HandleInput(index, e))\"\n                           @onkeydown=\"@(e => HandleKeyDown(index, e))\"\n                           @onkeydown:stopPropagation=\"true\" />\n                }\n            </div>\n            @if (g < groups.Count - 1 && Separator is not null)\n            {\n                <span class=\"text-muted-foreground text-lg font-medium px-1\">@Separator</span>\n            }\n        }\n    }\n</div>\n\n@if (!InsideFormField)\n{\n    @if (EffectiveInvalid && !string.IsNullOrEmpty(ErrorText))\n    {\n        <p class=\"mt-1 text-xs text-destructive\">@ErrorText</p>\n    }\n    else if (!string.IsNullOrEmpty(HelperText))\n    {\n        <p class=\"mt-1 text-xs text-muted-foreground\">@HelperText</p>\n    }\n}\n\n@implements IAsyncDisposable\n@inject ComponentInteropService Interop\n\n@code {\n    [CascadingParameter] private FormField.FormFieldContext? FormField { get; set; }\n\n    [Parameter] public int Length { get; set; } = 6;\n    [Parameter] public string Value { get; set; } = \"\";\n    [Parameter] public EventCallback<string> ValueChanged { get; set; }\n    [Parameter] public EventCallback<string> OnComplete { get; set; }\n    [Parameter] public string InputMode { get; set; } = \"Numeric\";\n    [Parameter] public bool Disabled { get; set; }\n    [Parameter] public bool Masked { get; set; }\n    [Parameter] public string? Separator { get; set; }\n    [Parameter] public int[]? GroupSizes { get; set; }\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 _baseId = $\"otp-{Guid.NewGuid():N}\";\n\n    private string CssClass => $\"flex items-center gap-2 {Class}\".Trim();\n\n    private string InputModeAttr => InputMode switch\n    {\n        \"Alphanumeric\" => \"text\",\n        \"Password\" => \"text\",\n        _ => \"numeric\"\n    };\n\n    private List<List<int>> GetGroups()\n    {\n        var groups = new List<List<int>>();\n        if (GroupSizes is not null && GroupSizes.Length > 0)\n        {\n            var idx = 0;\n            foreach (var size in GroupSizes)\n            {\n                var group = new List<int>();\n                for (var i = 0; i < size && idx < Length; i++, idx++)\n                {\n                    group.Add(idx);\n                }\n                if (group.Count > 0) groups.Add(group);\n            }\n            // Handle any remaining inputs not covered by GroupSizes\n            if (idx < Length)\n            {\n                var remaining = new List<int>();\n                for (; idx < Length; idx++) remaining.Add(idx);\n                groups.Add(remaining);\n            }\n        }\n        else\n        {\n            var all = new List<int>();\n            for (var i = 0; i < Length; i++) all.Add(i);\n            groups.Add(all);\n        }\n        return groups;\n    }\n\n    private string GetChar(int index) => index < Value.Length ? Value[index].ToString() : \"\";\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (!firstRender) return;\n        try\n        {\n            await Interop.RegisterOtpPaste(_baseId, Length, HandlePaste);\n        }\n        catch (JSDisconnectedException) { }\n        catch (ObjectDisposedException) { }\n    }\n\n    private async Task HandlePaste(string text)\n    {\n        var filtered = FilterInput(text);\n        var padded = filtered.PadRight(Length).Substring(0, Length);\n        Value = padded.TrimEnd();\n        await ValueChanged.InvokeAsync(Value);\n\n        if (Value.Length == Length)\n        {\n            await OnComplete.InvokeAsync(Value);\n        }\n        else if (Value.Length > 0)\n        {\n            await Interop.FocusElement($\"{_baseId}-{Value.Length}\");\n        }\n    }\n\n    private bool IsValidChar(char c) => InputMode switch\n    {\n        \"Numeric\" => char.IsDigit(c),\n        \"Alphanumeric\" => char.IsLetterOrDigit(c),\n        _ => true // Password mode allows any character\n    };\n\n    private string FilterInput(string input)\n    {\n        return new string(input.Where(IsValidChar).ToArray());\n    }\n\n    private async Task HandleInput(int index, ChangeEventArgs e)\n    {\n        var input = e.Value?.ToString() ?? \"\";\n        if (input.Length > 0)\n        {\n            var lastChar = input[^1];\n            if (!IsValidChar(lastChar)) return;\n\n            var chars = Value.PadRight(Length).ToCharArray();\n            chars[index] = lastChar;\n            Value = new string(chars).TrimEnd();\n            await ValueChanged.InvokeAsync(Value);\n\n            if (index < Length - 1)\n            {\n                await Interop.FocusElement($\"{_baseId}-{index + 1}\");\n            }\n\n            if (Value.Length == Length)\n            {\n                await OnComplete.InvokeAsync(Value);\n            }\n        }\n    }\n\n    private async Task HandleKeyDown(int index, KeyboardEventArgs e)\n    {\n        if (e.Key == \"Backspace\" || e.Key == \"Delete\")\n        {\n            var chars = Value.PadRight(Length).ToCharArray();\n            if (GetChar(index) != \"\")\n            {\n                chars[index] = ' ';\n                Value = new string(chars).TrimEnd();\n                await ValueChanged.InvokeAsync(Value);\n                if (index > 0)\n                    await Interop.FocusElement($\"{_baseId}-{index - 1}\");\n            }\n            else if (index > 0)\n            {\n                chars[index - 1] = ' ';\n                Value = new string(chars).TrimEnd();\n                await ValueChanged.InvokeAsync(Value);\n                await Interop.FocusElement($\"{_baseId}-{index - 1}\");\n            }\n        }\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        try\n        {\n            await Interop.UnregisterOtpPaste(_baseId, Length);\n        }\n        catch (JSDisconnectedException) { }\n    }\n}\n"
    }
  ],
  "slots": [],
  "serviceDependencies": [
    {
      "service": "ComponentInteropService",
      "namespace": ""
    }
  ],
  "cascadingDependencies": [
    {
      "name": "FormField",
      "type": "FormField.FormFieldContext?",
      "required": false
    }
  ],
  "relatedComponents": [
    {
      "name": "List",
      "reason": "used internally"
    }
  ],
  "keyboardInteractions": [
    {
      "key": "Backspace",
      "action": "HandleKeyDown handles Backspace in OtpInput.razor"
    },
    {
      "key": "Delete",
      "action": "HandleKeyDown handles Delete in OtpInput.razor"
    }
  ],
  "tests": [
    "tests/Lumeo.Tests/Components/OtpInput/OtpInputTests.cs"
  ],
  "mdSummary": "# OtpInput\n\nOne-time password input, auto-advances between boxes.\n\n**Package:** `Lumeo` · **Category:** Forms/Inputs\n**Docs:** https://lumeo.nativ.sh/components/otp-input\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| OtpInput | FormField | `FormField.FormFieldContext?` | `` |  |\n| OtpInput | Length | `int` | `6` |  |\n| OtpInput | Value | `string` | `\"\"` |  |\n| OtpInput | ValueChanged | `EventCallback<string>` | `` |  |\n| OtpInput | OnComplete | `EventCallback<string>` | `` |  |\n| OtpInput | InputMode | `string` | `\"Numeric\"` |  |\n| OtpInput | Disabled | `bool` | `` |  |\n| OtpInput | Masked | `bool` | `` |  |\n| OtpInput | Separator | `string?` | `` |  |\n| OtpInput | GroupSizes | `int[]?` | `` |  |\n| OtpInput | Required | `bool` | `` |  |\n| OtpInput | Invalid | `bool` | `` |  |\n| OtpInput | ErrorText | `string?` | `` |  |\n| OtpInput | HelperText | `string?` | `` |  |\n| OtpInput | Label | `string?` | `` |  |\n| OtpInput | Name | `string?` | `` |  |\n| OtpInput | Class | `string?` | `` |  |\n| OtpInput | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- (none)\n\n## Events\n- `ValueChanged` (`EventCallback<string>`)\n- `OnComplete` (`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- `Backspace` — HandleKeyDown handles Backspace in OtpInput.razor\n- `Delete` — HandleKeyDown handles Delete in OtpInput.razor\n\n## Example\n```razor\n<OtpInput />\n```\n\n## CSS variables\n`--color-destructive`, `--color-input`, `--color-muted`, `--color-muted-foreground`, `--radius`\n\n## Files\n- `UI/OtpInput/OtpInput.razor`\n"
}