{
  "$schema": "https://mian.ro/schema/v2/registry.json",
  "schemaVersion": "2.0.0",
  "generatedAt": "2026-10-02T13:14:06.957Z",
  "id": "number-input",
  "name": "Number input",
  "category": "Forms",
  "description": "A stepper: a numeric input between decrement and increment buttons. Respects min, max and step; hold to repeat.",
  "tags": [
    "number",
    "stepper",
    "quantity",
    "counter",
    "increment"
  ],
  "requiresJs": true,
  "license": "LicenseRef-MIAN-1.0",
  "composition": {
    "role": null,
    "intents": [
      "quantity stepper",
      "plus minus quantity",
      "number of guests",
      "cart quantity"
    ],
    "prominence": null,
    "density": null,
    "worksAfter": [],
    "worksBefore": [],
    "alternatives": [],
    "avoid": null
  },
  "url": "https://mian.ro/components/number-input",
  "markdownUrl": "https://mian.ro/ai/components/number-input.md",
  "classes": [
    {
      "name": "mn-number",
      "description": "Wrapper."
    },
    {
      "name": "[data-mn-step=\"-1\" | \"1\"]",
      "description": "The two buttons."
    }
  ],
  "accessibility": [
    "The input stays a native number input; ArrowUp and ArrowDown step it. The buttons are skipped by Tab to avoid three stops for one value."
  ],
  "examples": [
    {
      "title": "Quantity",
      "description": null,
      "html": "<div class=\"mn-number\">\n  <button type=\"button\" data-mn-step=\"-1\" aria-label=\"Decrease\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M5 12h14\"/></svg></button>\n  <input type=\"number\" value=\"1\" min=\"1\" max=\"10\" aria-label=\"Quantity\">\n  <button type=\"button\" data-mn-step=\"1\" aria-label=\"Increase\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 5v14M5 12h14\"/></svg></button>\n</div>"
    },
    {
      "title": "Decimal step",
      "description": null,
      "html": "<div class=\"mn-number\">\n  <button type=\"button\" data-mn-step=\"-1\" aria-label=\"Decrease\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M5 12h14\"/></svg></button>\n  <input type=\"number\" value=\"1.5\" min=\"0\" max=\"5\" step=\"0.25\" aria-label=\"Weight in kilograms\">\n  <button type=\"button\" data-mn-step=\"1\" aria-label=\"Increase\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 5v14M5 12h14\"/></svg></button>\n</div>"
    }
  ],
  "css": ".mn-number { display: inline-flex; align-items: stretch; height: var(--mn-size-md); max-width: 100%;\n  border: var(--mn-border-w) solid var(--mn-input); border-radius: var(--mn-radius-field);\n  background: var(--mn-surface); box-shadow: var(--mn-shadow-sm); overflow: hidden; }\n.mn-number:focus-within { border-color: var(--mn-ring); box-shadow: 0 0 0 3px color-mix(in oklab, var(--mn-ring) 25%, transparent); }\n.mn-number > input {\n  width: 4.5rem; min-width: 0; border: 0; padding: 0 .5rem; background: transparent; color: var(--mn-fg);\n  font: 500 var(--mn-text-sm)/1 var(--mn-font); text-align: center; font-variant-numeric: tabular-nums;\n  -moz-appearance: textfield; appearance: textfield;\n}\n.mn-number > input::-webkit-inner-spin-button, .mn-number > input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }\n.mn-number > input:focus { outline: none; }\n.mn-number > button {\n  display: grid; place-items: center; width: var(--mn-size-md); flex: none;\n  border: 0; background: var(--mn-surface-2); color: var(--mn-fg); cursor: pointer; font-size: 1.1rem;\n}\n.mn-number > button:hover { background: color-mix(in oklab, var(--mn-surface-2), var(--mn-fg) 7%); }\n.mn-number > button:disabled { opacity: .4; cursor: not-allowed; }\n.mn-number > button:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--mn-ring); }\n.mn-number > button svg { width: 1rem; height: 1rem; }",
  "js": "register('number-input', (root) => {\n  each(root, '.mn-number', 'Num', (w) => {\n    const input = w.querySelector('input');\n    if (!input) return;\n    const dec = w.querySelector('[data-mn-step=\"-1\"]'), inc = w.querySelector('[data-mn-step=\"1\"]');\n    const num = (v, d) => (v === '' || v == null || isNaN(+v) ? d : +v);\n    const sync = () => {\n      const v = num(input.value, 0), min = num(input.min, -Infinity), max = num(input.max, Infinity);\n      if (dec) dec.disabled = input.disabled || v <= min;\n      if (inc) inc.disabled = input.disabled || v >= max;\n    };\n    const step = (dir) => {\n      const s = num(input.step, 1), min = num(input.min, -Infinity), max = num(input.max, Infinity);\n      const decimals = (String(s).split('.')[1] || '').length;\n      const v = Math.min(max, Math.max(min, num(input.value, 0) + dir * s));\n      input.value = v.toFixed(decimals);\n      input.dispatchEvent(new Event('input', { bubbles: true }));\n      input.dispatchEvent(new Event('change', { bubbles: true }));\n      sync();\n    };\n    let hold = 0, rep = 0;\n    const stop = () => { clearTimeout(hold); clearInterval(rep); };\n    [dec, inc].forEach((b) => {\n      if (!b) return;\n      b.tabIndex = -1;   // the input owns keyboard focus; ArrowUp/Down step it\n      b.setAttribute('aria-controls', id(input, 'mn-num'));\n      b.addEventListener('pointerdown', (e) => {\n        e.preventDefault();\n        const dir = +b.dataset.mnStep;\n        step(dir);\n        hold = setTimeout(() => { rep = setInterval(() => step(dir), 60); }, 400);\n      });\n      b.addEventListener('pointerup', stop); b.addEventListener('pointerleave', stop);\n      b.addEventListener('click', (e) => { if (e.detail === 0) step(+b.dataset.mnStep); });\n    });\n    input.addEventListener('input', sync);\n    input.addEventListener('keydown', (e) => {\n      if (e.key === 'ArrowUp') { e.preventDefault(); step(1); }\n      else if (e.key === 'ArrowDown') { e.preventDefault(); step(-1); }\n    });\n    sync();\n  });\n});",
  "install": {
    "css": "https://mian.ro/dist/mian.css",
    "js": "https://mian.ro/dist/mian.js",
    "registryItem": "https://mian.ro/r/number-input.json"
  }
}
