{
  "$schema": "https://mian.ro/schema/v2/registry.json",
  "schemaVersion": "2.0.0",
  "generatedAt": "2026-10-02T13:14:06.957Z",
  "id": "otp-input",
  "name": "OTP input",
  "category": "Forms",
  "description": "One-time-code entry. Auto-advances, handles backspace and paste, supports SMS autofill on the first box.",
  "tags": [
    "otp",
    "pin",
    "verification code",
    "2fa",
    "one time code"
  ],
  "requiresJs": true,
  "license": "LicenseRef-MIAN-1.0",
  "composition": {
    "role": null,
    "intents": [
      "verification code",
      "one time password",
      "2fa code",
      "pin entry"
    ],
    "prominence": null,
    "density": null,
    "worksAfter": [],
    "worksBefore": [],
    "alternatives": [],
    "avoid": null
  },
  "url": "https://mian.ro/components/otp-input",
  "markdownUrl": "https://mian.ro/ai/components/otp-input.md",
  "classes": [
    {
      "name": "mn-otp",
      "description": "Wrapper around N inputs."
    },
    {
      "name": "data-mn-otp=\"alphanumeric\"",
      "description": "Allow letters too."
    },
    {
      "name": "mn-otp-sep",
      "description": "Visual separator."
    }
  ],
  "accessibility": [
    "Wrap in a fieldset or give the group role=\"group\" and a label. Each box gets \"Character n of N\". Emits mn:complete with the full code."
  ],
  "examples": [
    {
      "title": "Six digits",
      "description": null,
      "html": "<div class=\"mn-otp\" role=\"group\" aria-label=\"Verification code\">\n  <input><input><input><span class=\"mn-otp-sep\" aria-hidden=\"true\">–</span><input><input><input>\n</div>"
    }
  ],
  "css": ".mn-otp { display: inline-flex; gap: .5rem; align-items: center; max-width: 100%; }\n.mn-otp > input {\n  flex: 0 1 auto; min-width: 0;\n  width: calc(var(--mn-size-md) + 4px); height: calc(var(--mn-size-md) + 8px); padding: 0;\n  border: var(--mn-border-w) solid var(--mn-input); border-radius: var(--mn-radius-field);\n  background: var(--mn-surface); color: var(--mn-fg); box-shadow: var(--mn-shadow-sm);\n  font: 600 calc(var(--mn-text-sm) + 4px)/1 var(--mn-font-mono); text-align: center;\n  caret-color: var(--mn-ring);\n}\n.mn-otp > input:focus { outline: none; border-color: var(--mn-ring); box-shadow: 0 0 0 3px color-mix(in oklab, var(--mn-ring) 25%, transparent); }\n.mn-otp-sep { color: var(--mn-muted); }",
  "js": "register('otp-input', (root) => {\n  each(root, '.mn-otp', 'Otp', (w) => {\n    const boxes = [...w.querySelectorAll('input')];\n    const alnum = w.dataset.mnOtp === 'alphanumeric';\n    const ok = (c) => (alnum ? /^[a-z0-9]$/i.test(c) : /^\\d$/.test(c));\n    boxes.forEach((b, i) => {\n      b.maxLength = 1;\n      b.autocomplete = i === 0 ? 'one-time-code' : 'off';\n      b.inputMode = alnum ? 'text' : 'numeric';\n      if (!b.getAttribute('aria-label')) b.setAttribute('aria-label', `Character ${i + 1} of ${boxes.length}`);\n      b.addEventListener('focus', () => b.select());\n      b.addEventListener('input', () => {\n        const v = b.value.slice(-1);\n        b.value = ok(v) ? v.toUpperCase() : '';\n        if (b.value && boxes[i + 1]) boxes[i + 1].focus();\n        const code = boxes.map((x) => x.value).join('');\n        if (code.length === boxes.length) emit(w, 'complete', { value: code });\n      });\n      b.addEventListener('keydown', (e) => {\n        if (e.key === 'Backspace' && !b.value && boxes[i - 1]) { boxes[i - 1].focus(); boxes[i - 1].value = ''; e.preventDefault(); }\n        else if (e.key === 'ArrowLeft' && boxes[i - 1]) { e.preventDefault(); boxes[i - 1].focus(); }\n        else if (e.key === 'ArrowRight' && boxes[i + 1]) { e.preventDefault(); boxes[i + 1].focus(); }\n      });\n      b.addEventListener('paste', (e) => {\n        const text = (e.clipboardData?.getData('text') || '').split('').filter(ok);\n        if (!text.length) return;\n        e.preventDefault();\n        boxes.forEach((x, k) => { if (k >= i && text[k - i]) x.value = text[k - i].toUpperCase(); });\n        (boxes[Math.min(boxes.length - 1, i + text.length)] || b).focus();\n        const code = boxes.map((x) => x.value).join('');\n        if (code.length === boxes.length) emit(w, 'complete', { value: code });\n      });\n    });\n  });\n});",
  "install": {
    "css": "https://mian.ro/dist/mian.css",
    "js": "https://mian.ro/dist/mian.js",
    "registryItem": "https://mian.ro/r/otp-input.json"
  }
}
