{
  "generatedFrom": "number-with-unit.contract.json",
  "component": "Number with unit",
  "slug": "number-with-unit",
  "group": "Settings",
  "tier": "leaf-lit",
  "summary": "A fixed-width digit input with the unit written IN FULL inline (rendered exactly as passed — \"seconds\", \"points\" — muted grey), a hover ▲/▼ stepper, a hard digit cap, clamp-on-change, and an error line — the settings field for a number that carries a unit.",
  "install": {
    "package": "@ahaslides-product/design",
    "registry": "https://npm.pkg.github.com",
    "scope": "@ahaslides-product",
    "auth": "Published to GitHub Packages — needs a GitHub token with read:packages. Configure the @ahaslides-product scope in .npmrc before installing.",
    "npmrc": "@ahaslides-product:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}   # a GitHub token with read:packages",
    "command": "npm i @ahaslides-product/design",
    "tokenLayer": "import '@ahaslides-product/design/tokens.css';",
    "import": "import '@ahaslides-product/design/aha-number-with-unit';",
    "element": "aha-number-with-unit",
    "registers": "aha-number-with-unit"
  },
  "feeds": {
    "doc": "https://ahaslides-product.github.io/ahaslides-design/number-with-unit/index.html",
    "md": "https://ahaslides-product.github.io/ahaslides-design/number-with-unit/number-with-unit.md",
    "agentJson": "https://ahaslides-product.github.io/ahaslides-design/number-with-unit.agent.json",
    "llms": "https://ahaslides-product.github.io/ahaslides-design/number-with-unit.llms.txt",
    "index": "https://ahaslides-product.github.io/ahaslides-design/llms.txt"
  },
  "recommendedSnippet": "html",
  "vibeCode": "Emit the HTML snippet by default — <aha-number-with-unit> is a standard custom element that renders on open — React/Vue are thin adapters over the same element. Paste into an .html file and it renders with no build step.",
  "frameworks": {
    "react": {
      "via": "web-component",
      "ref": "aha-number-with-unit"
    },
    "vue": {
      "via": "web-component",
      "ref": "aha-number-with-unit"
    },
    "html": {
      "via": "web-component",
      "ref": "aha-number-with-unit",
      "runnable": "paste-and-run — no build step, renders on open"
    }
  },
  "props": [
    {
      "name": "value",
      "type": "number",
      "default": "—",
      "desc": "Controlled numeric value"
    },
    {
      "name": "unit",
      "type": "string",
      "default": "—",
      "desc": "Inline suffix — written IN FULL, rendered exactly as passed (seconds, points, %)"
    },
    {
      "name": "min",
      "type": "number",
      "default": "—",
      "desc": "Lower clamp bound"
    },
    {
      "name": "max",
      "type": "number",
      "default": "—",
      "desc": "Upper clamp bound"
    },
    {
      "name": "step",
      "type": "number",
      "default": "1",
      "desc": "Stepper / arrow-key increment"
    },
    {
      "name": "maxdigits",
      "type": "number",
      "default": "4",
      "desc": "Hard digit cap — a keystroke past this many digits is ignored"
    },
    {
      "name": "size",
      "type": "small | default | large",
      "default": "default",
      "desc": "Control height — small 24 · default 32 · large 40; radius stays 8"
    },
    {
      "name": "errormessage",
      "type": "string",
      "default": "—",
      "desc": "Presence draws the red border/ring and the message line below"
    },
    {
      "name": "disabled",
      "type": "boolean",
      "default": "false",
      "desc": "Non-interactive; grey fill"
    },
    {
      "name": "change",
      "type": "CustomEvent<{value}>",
      "default": "—",
      "desc": "Composed event on commit / stepper / arrow; read e.detail.value (clamped number)"
    }
  ],
  "tokens": [
    "border",
    "border-hover",
    "color-primary",
    "bg-container",
    "bg-container-disabled",
    "bg-accent",
    "text-default",
    "text-tertiary",
    "text-disabled",
    "icon-muted",
    "radius-default",
    "radius-xs",
    "focus-ring-soft",
    "border-error",
    "color-error",
    "border-disabled"
  ],
  "spec": [
    {
      "label": "Box",
      "value": "height 32 (default) · radius 8 · digit input + inline unit + stepper"
    },
    {
      "label": "Sizes",
      "value": "small 24 · default 32 · large 40 — all radius 8"
    },
    {
      "label": "Unit",
      "value": "14/21 #8A8A8A inline suffix; written IN FULL, rendered exactly as passed (seconds, points)"
    },
    {
      "label": "Stepper",
      "value": "▲/▼ tertiary (system-caret-up/down), revealed on hover/focus; disabled at min/max"
    },
    {
      "label": "Digit cap",
      "value": "maxDigits (default 4) — a keystroke past the cap is dropped; numeric only"
    },
    {
      "label": "Clamp",
      "value": "value clamps to min/max on change AND blur (immediate fallback)"
    },
    {
      "label": "Error",
      "value": "#F5222D border + ring + a 12/18 #F5222D message line below; sets aria-invalid"
    },
    {
      "label": "Focus",
      "value": "#6A1EBB border + 2px #D3B4FF@30% ring (persistent wrapper)"
    },
    {
      "label": "Disabled",
      "value": "#F1F1F1 fill, #EBEBEB border, not-allowed"
    }
  ],
  "opinion": {
    "whenToUse": [
      {
        "what": "Number with unit",
        "when": "a number the host does NOT provide that carries a unit — a per-item timer, points-per-answer, a spin duration"
      },
      {
        "what": "Host time limit",
        "when": "a standard whole-slide countdown — use the host-native enableTimeLimit, not this"
      },
      {
        "what": "Select",
        "when": "the unit itself is changeable — a unit dropdown, not an inline suffix"
      }
    ],
    "note": "A fixed unit is an inline suffix inside the field (SETTINGS-41) — a bordered box beside the input implies the unit is selectable. Write the unit IN FULL, rendered exactly as passed (`seconds`, `points`) — never truncated to a short label. Ship pre-filled with the recommended default and clamp immediately.",
    "dsv3": "COVERED — the settings-lab NumberWithUnit control mapped into the DS. Box, border and focus ring reuse the DS V3 field family; the inline unit, hover stepper, digit cap, clamp-on-change/blur and errorMessage line are the settings-specific additions (SETTINGS-45/46/41/25)."
  },
  "surfaces": [
    "editor",
    "settings"
  ],
  "snippets": {
    "html": "<!-- Paste-and-run: save as .html and open in a browser. No build step.\n     <aha-number-with-unit> is the SAME shared custom element React and Vue consume — here in its\n     native form. Theming comes only from the --aha-* tokens in tokens.css. Hover the field to reveal\n     the ▲/▼ stepper; the unit is written inline IN FULL — rendered exactly as passed (\"seconds\", \"points\"). -->\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css\">\n<script type=\"module\">\n  import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-number-with-unit.js';   // registers <aha-number-with-unit>\n</script>\n\n<!-- a per-question timer: unit written in full, clamps to 5–120, hard cap of 4 digits -->\n<aha-number-with-unit id=\"timer\" value=\"20\" unit=\"seconds\" min=\"5\" max=\"120\" maxdigits=\"4\"></aha-number-with-unit>\n\n<!-- points, and a field carrying an error message -->\n<aha-number-with-unit value=\"1000\" unit=\"points\" min=\"0\" max=\"9999\" maxdigits=\"4\"></aha-number-with-unit>\n<aha-number-with-unit value=\"200\" unit=\"seconds\" min=\"5\" max=\"120\" errormessage=\"Must be 120 or less.\"></aha-number-with-unit>\n\n<script>\n  // Plain DOM — no framework. e.detail.value is the clamped number.\n  document.getElementById('timer').addEventListener('change', (e) => {\n    console.log('seconds:', e.detail.value);\n  });\n</script>\n",
    "react": "import '@ahaslides-product/design/aha-number-with-unit'; // registers <aha-number-with-unit>\nimport { useRef, useEffect } from 'react';\n\n// React 18 needs a thin wrapper; React 19 can use <aha-number-with-unit> directly.\n// e.detail.value is the clamped number; the unit is written inline IN FULL — rendered exactly as passed (\"seconds\").\nfunction AhaNumberWithUnit({ value, unit, min, max, step, maxDigits, size, errorMessage, disabled, onChange }) {\n  const ref = useRef();\n  useEffect(() => {\n    const el = ref.current;\n    el.value = value == null ? '' : String(value);\n    el.disabled = !!disabled;\n    const h = (e) => onChange?.(e.detail.value);   // composed CustomEvent\n    el.addEventListener('change', h);\n    return () => el.removeEventListener('change', h);\n  });\n  return (\n    <aha-number-with-unit\n      ref={ref}\n      unit={unit}\n      {...(min != null ? { min: String(min) } : {})}\n      {...(max != null ? { max: String(max) } : {})}\n      {...(step != null ? { step: String(step) } : {})}\n      {...(maxDigits ? { maxdigits: String(maxDigits) } : {})}\n      {...(size ? { size } : {})}\n      {...(errorMessage ? { errormessage: errorMessage } : {})}\n    />\n  );\n}\n\n// usage\n<AhaNumberWithUnit value={20} unit=\"seconds\" min={5} max={120} maxDigits={4} onChange={setSeconds} />\n",
    "vue": "// main.ts — register the element + mark aha-* as custom elements\nimport '@ahaslides-product/design/aha-number-with-unit';   // registers <aha-number-with-unit>\napp.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');\n\n// Component.vue\n<script setup>\nimport { ref } from 'vue';\nconst seconds = ref(20);\n</script>\n\n<template>\n  <!-- the unit is written inline IN FULL — rendered exactly as passed (\"seconds\"); e.detail.value is the clamped number -->\n  <aha-number-with-unit\n    unit=\"seconds\"\n    min=\"5\"\n    max=\"120\"\n    maxdigits=\"4\"\n    :value.prop=\"String(seconds)\"\n    @change=\"seconds = $event.detail.value\"\n  />\n</template>\n"
  }
}
