{
  "generatedFrom": "counted-textarea.contract.json",
  "component": "Counted textarea",
  "slug": "counted-textarea",
  "group": "Settings",
  "tier": "leaf-lit",
  "summary": "The multi-line settings field — a CountedInput that wraps to several lines, grows from minRows to maxRows then scrolls, with the same focus-only character counter.",
  "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-counted-textarea';",
    "element": "aha-counted-textarea",
    "registers": "aha-counted-textarea"
  },
  "feeds": {
    "doc": "https://ahaslides-product.github.io/ahaslides-design/counted-textarea/index.html",
    "md": "https://ahaslides-product.github.io/ahaslides-design/counted-textarea/counted-textarea.md",
    "agentJson": "https://ahaslides-product.github.io/ahaslides-design/counted-textarea.agent.json",
    "llms": "https://ahaslides-product.github.io/ahaslides-design/counted-textarea.llms.txt",
    "index": "https://ahaslides-product.github.io/ahaslides-design/llms.txt"
  },
  "recommendedSnippet": "html",
  "vibeCode": "Emit the HTML snippet by default — <aha-counted-textarea> 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-counted-textarea"
    },
    "vue": {
      "via": "web-component",
      "ref": "aha-counted-textarea"
    },
    "html": {
      "via": "web-component",
      "ref": "aha-counted-textarea",
      "runnable": "paste-and-run — no build step, renders on open"
    }
  },
  "props": [
    {
      "name": "value",
      "type": "string",
      "default": "\"\"",
      "desc": "Controlled text value"
    },
    {
      "name": "placeholder",
      "type": "string",
      "default": "—",
      "desc": "Hint shown when empty"
    },
    {
      "name": "maxlength",
      "type": "number",
      "default": "150",
      "desc": "Character cap (settings-lab default 150); drives the focus-only counter (shows current/max). Pass `none` to drop the cap and hide the counter"
    },
    {
      "name": "minrows",
      "type": "number",
      "default": "2",
      "desc": "Rows shown at rest — the field never shrinks below this"
    },
    {
      "name": "maxrows",
      "type": "number",
      "default": "5",
      "desc": "Rows the field grows to before it scrolls internally instead of growing"
    },
    {
      "name": "status",
      "type": "default | error",
      "default": "default",
      "desc": "Error recolours the border + ring and sets aria-invalid"
    },
    {
      "name": "disabled",
      "type": "boolean",
      "default": "false",
      "desc": "Non-interactive; grey fill"
    },
    {
      "name": "readonly",
      "type": "boolean",
      "default": "false",
      "desc": "Selectable but not editable; secondary fill"
    },
    {
      "name": "borderless",
      "type": "boolean",
      "default": "false",
      "desc": "Drop the field's own border/ring/fill — for composing inside a parent that carries the one border (OptionRow, SETTINGS-34)"
    },
    {
      "name": "input",
      "type": "CustomEvent<{value}>",
      "default": "—",
      "desc": "Composed event on each keystroke; read e.detail.value"
    },
    {
      "name": "change",
      "type": "CustomEvent<{value}>",
      "default": "—",
      "desc": "Composed event on commit (blur)"
    }
  ],
  "tokens": [
    "border",
    "border-hover",
    "color-primary",
    "bg-container",
    "bg-container-secondary",
    "bg-container-disabled",
    "text-default",
    "text-tertiary",
    "text-disabled",
    "radius-default",
    "focus-ring-soft",
    "border-error",
    "color-error",
    "border-disabled"
  ],
  "spec": [
    {
      "label": "Box",
      "value": "radius 8 · padding 6 12 · resize off"
    },
    {
      "label": "Autogrow",
      "value": "minrows (default 2) → maxrows (default 5); beyond maxrows scrolls internally, never grows unbounded"
    },
    {
      "label": "Rest border",
      "value": "1px #E3E3E3"
    },
    {
      "label": "Focus",
      "value": "#6A1EBB border + 2px #D3B4FF@30% ring (on the persistent wrapper)"
    },
    {
      "label": "Counter",
      "value": "12/18 #8A8A8A, overlay bottom-right; opacity 0 at rest → 1 on focus"
    },
    {
      "label": "Reveal",
      "value": "focus-only (:host([_focused][_counted])) — hidden at rest; space always reserved (no shift)"
    },
    {
      "label": "Error",
      "value": "#F5222D border + error-tinted ring; sets aria-invalid"
    },
    {
      "label": "Disabled",
      "value": "#F1F1F1 fill, #EBEBEB border, not-allowed"
    },
    {
      "label": "Text",
      "value": "Plus Jakarta 14/21; placeholder #8A8A8A"
    }
  ],
  "opinion": {
    "whenToUse": [
      {
        "what": "Counted textarea",
        "when": "a multi-line settings field — a description, a long answer — that needs a character cap; the counter appears only on focus"
      },
      {
        "what": "Counted input",
        "when": "the single-line sibling — option/answer/label text with a cap"
      },
      {
        "what": "Input",
        "when": "a general single-line field with no counter (names, search)"
      }
    ],
    "note": "Never show a permanent counter on every field — it reads as heavy. The counter belongs inside the field, revealed on focus. Cap the growth: a textarea that grows without bound pushes the rest of the panel down; grow to maxRows then scroll.",
    "dsv3": "COVERED — the settings-lab CountedTextarea control mapped into the DS. Box, border, focus ring and error status reuse the DS V3 field family; the autogrow-then-scroll behaviour and the focus-only bottom-right counter are the settings-specific additions (SETTINGS-21/42)."
  },
  "surfaces": [
    "editor",
    "settings"
  ],
  "snippets": {
    "html": "<!-- Paste-and-run: save as .html and open in a browser. No build step.\n     <aha-counted-textarea> 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. The character counter is\n     revealed only while the field is focused (SETTINGS-21/42) — click in to see it; type past a few\n     lines and the field scrolls instead of growing unbounded. -->\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-counted-textarea.js';   // registers <aha-counted-textarea>\n</script>\n\n<!-- maxlength turns the focus-only counter on; minrows/maxrows bound the autogrow -->\n<aha-counted-textarea id=\"desc\" maxlength=\"150\" minrows=\"2\" maxrows=\"5\"\n  value=\"A longer answer that wraps to several lines.\" placeholder=\"Answer\"></aha-counted-textarea>\n\n<!-- status: error   ·   disabled / readonly -->\n<aha-counted-textarea status=\"error\" maxlength=\"120\" value=\"Too long\" placeholder=\"Description\"></aha-counted-textarea>\n\n<script>\n  // Plain DOM — no framework. Read e.detail.value off the composed CustomEvent.\n  document.getElementById('desc').addEventListener('input', (e) => {\n    console.log('value:', e.detail.value);\n  });\n</script>\n",
    "react": "import '@ahaslides-product/design/aha-counted-textarea'; // registers <aha-counted-textarea>\nimport { useRef, useEffect } from 'react';\n\n// React 18 needs a thin wrapper; React 19 can use <aha-counted-textarea> directly.\n// The counter shows only on focus when maxLength is set; the field grows minRows→maxRows then scrolls.\nfunction AhaCountedTextarea({ value, placeholder, maxLength, minRows, maxRows, status, disabled, readOnly, onChange }) {\n  const ref = useRef();\n  useEffect(() => {\n    const el = ref.current;\n    el.value = value ?? '';\n    el.disabled = !!disabled;\n    const h = (e) => onChange?.(e.detail.value);   // composed CustomEvent\n    el.addEventListener('input', h);\n    return () => el.removeEventListener('input', h);\n  });\n  return (\n    <aha-counted-textarea\n      ref={ref}\n      placeholder={placeholder}\n      {...(maxLength ? { maxlength: String(maxLength) } : {})}\n      {...(minRows ? { minrows: String(minRows) } : {})}\n      {...(maxRows ? { maxrows: String(maxRows) } : {})}\n      {...(status ? { status } : {})}\n      {...(readOnly ? { readonly: '' } : {})}\n    />\n  );\n}\n\n// usage\n<AhaCountedTextarea value={answer} placeholder=\"Answer\" maxLength={150} minRows={2} maxRows={5} onChange={setAnswer} />\n",
    "vue": "// main.ts — register the element + mark aha-* as custom elements\nimport '@ahaslides-product/design/aha-counted-textarea';   // registers <aha-counted-textarea>\napp.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');\n\n// Component.vue\n<script setup>\nimport { ref } from 'vue';\nconst answer = ref('');\n</script>\n\n<template>\n  <!-- maxlength turns the focus-only counter on; minrows/maxrows bound the autogrow -->\n  <aha-counted-textarea\n    maxlength=\"150\"\n    minrows=\"2\"\n    maxrows=\"5\"\n    placeholder=\"Answer\"\n    :value.prop=\"answer\"\n    @input=\"answer = $event.detail.value\"\n  />\n</template>\n"
  }
}
