{
  "generatedFrom": "radio.contract.json",
  "component": "Radio",
  "slug": "radio",
  "group": "Data Entry",
  "tier": "leaf-lit",
  "summary": "Pick exactly one option from a small mutually-exclusive set, all shown at once.",
  "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-radio';",
    "element": "aha-radio",
    "registers": "aha-radio"
  },
  "feeds": {
    "doc": "https://ahaslides-product.github.io/ahaslides-design/radio/index.html",
    "md": "https://ahaslides-product.github.io/ahaslides-design/radio/radio.md",
    "agentJson": "https://ahaslides-product.github.io/ahaslides-design/radio.agent.json",
    "llms": "https://ahaslides-product.github.io/ahaslides-design/radio.llms.txt",
    "index": "https://ahaslides-product.github.io/ahaslides-design/llms.txt"
  },
  "recommendedSnippet": "html",
  "vibeCode": "Emit the HTML snippet by default — <aha-radio> 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-radio"
    },
    "vue": {
      "via": "web-component",
      "ref": "aha-radio"
    },
    "html": {
      "via": "web-component",
      "ref": "aha-radio",
      "runnable": "paste-and-run — no build step, renders on open"
    }
  },
  "props": [
    {
      "name": "checked",
      "type": "boolean",
      "default": "false",
      "desc": "Controlled selection"
    },
    {
      "name": "value",
      "type": "string",
      "default": "\"\"",
      "desc": "The value this option contributes to the group"
    },
    {
      "name": "name",
      "type": "string",
      "default": "—",
      "desc": "Groups radios; one `name` = one mutually-exclusive set"
    },
    {
      "name": "variant",
      "type": "'dot' | 'button' | 'card'",
      "default": "dot",
      "desc": "dot = ring + brand dot; button = segmented connected pill (Radio-Button-Group); card = the whole bordered card is the target (dot + title + optional description), selected card gets a brand border + bg-accent fill"
    },
    {
      "name": "description",
      "type": "string",
      "default": "—",
      "desc": "Card variant only — a secondary line rendered under the title"
    },
    {
      "name": "size",
      "type": "'default' | 'small'",
      "default": "default",
      "desc": "default = 16px ring / 32px button; small = 14px ring / 24px button"
    },
    {
      "name": "direction",
      "type": "'horizontal' | 'vertical'",
      "default": "horizontal",
      "desc": "How a group flows; in button variant also picks which edges the connected pills round + share"
    },
    {
      "name": "disabled",
      "type": "boolean",
      "default": "false",
      "desc": "Disables the control"
    },
    {
      "name": "change",
      "type": "CustomEvent<{value, checked}>",
      "default": "—",
      "desc": "Composed event; read e.detail.value"
    }
  ],
  "tokens": [
    "color-primary",
    "border-strong",
    "border-hover",
    "bg-container",
    "bg-container-disabled",
    "border-disabled",
    "bg-accent",
    "text-default",
    "text-secondary",
    "text-disabled",
    "font-product",
    "radius-default"
  ],
  "spec": [
    {
      "label": "Ring",
      "value": "16×16 circle · 1px #D4D4D4 border (small: 14×14)"
    },
    {
      "label": "Hover border",
      "value": "#D3B4FF (purple-30)"
    },
    {
      "label": "Checked",
      "value": "#6A1EBB ring + 8px brand dot (scales in)"
    },
    {
      "label": "Disabled",
      "value": "#F1F1F1 fill · #EBEBEB border"
    },
    {
      "label": "Label",
      "value": "Plus Jakarta 14/21 (small: 13/20)"
    },
    {
      "label": "Button variant",
      "value": "segmented pill · 32px (small 24) · checked = bg-accent + brand border, shared edge collapsed"
    },
    {
      "label": "Card variant",
      "value": "bordered card · dot + title + optional description · checked = brand border + bg-accent fill (16px pad, small 12px)"
    }
  ],
  "opinion": {
    "whenToUse": [
      {
        "what": "Radio",
        "when": "exactly one from a small (2–5) mutually-exclusive set, all worth showing at once"
      },
      {
        "what": "Select",
        "when": "one value from a known set of more than ~5 options — don't crowd the surface"
      },
      {
        "what": "Checkbox",
        "when": "one or more independent options that don't exclude each other"
      }
    ],
    "note": "Pre-select a sensible default rather than leaving the group empty. Labels are sentence case and state the choice — 'Show results after each question', not 'Show Results'. Keep product nouns: Q&A, Word cloud. Reach for variant='button' when the options are short, mutually-exclusive views (Grid/List, Day/Week/Month) that read better as a segmented control. Reach for variant='card' when each option carries a title plus a line of explanation and the whole tile should be the click target (plan pickers, mode choosers).",
    "dsv3": "COVERED — enumerated from the DS V3 Radio component-set taxonomy: Radio-Group / Radio Group (dot radio + label, states rest·hover·checked·unchecked·disabled), Radio-Button-Group & Components/Radio-Button/Solid & Form-Item/Segmented-radio (the segmented connected-pill button variant), and Radio-Card (the whole bordered card is the selectable target — dot + title + optional description; selected = brand border + bg-accent fill), with size (default·small) and group direction (horizontal·vertical). Measured reference (radio|rest·hover·checked·disabled): 16px ring, rest border #D4D4D4, hover border purple-30, checked brand ring + brand dot, disabled #F1F1F1 fill / #EBEBEB border. Token layer: color-primary dot + button fill + card selected border (bg-accent fill), border-strong rest, border-hover hover, border-disabled + bg-container-disabled disabled, radius-default pills + cards, text-secondary card description. Motion bound to shared motion tokens on persistent nodes (dot scale, ring/button border-color, card border-color + background)."
  },
  "surfaces": [
    "editor",
    "dashboard",
    "settings",
    "audience"
  ],
  "snippets": {
    "html": "<!-- Paste-and-run: save as .html and open in a browser. No build step.\n     <aha-radio> is the SAME shared custom element React and Vue consume.\n     Radios sharing a `name` are mutually exclusive; it emits a composed `change` event. -->\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-radio.js';   // registers <aha-radio>\n</script>\n\n<!-- Wrap the set in role=\"radiogroup\" so assistive tech announces one group.\n     Arrow keys move + select within it; Tab moves in and out as a single stop. -->\n<div role=\"radiogroup\" aria-label=\"Activity mode\" style=\"display:flex;flex-direction:column;gap:10px\">\n  <aha-radio name=\"mode\" value=\"poll\" checked>Poll</aha-radio>\n  <aha-radio name=\"mode\" value=\"quiz\">Quiz</aha-radio>\n  <aha-radio name=\"mode\" value=\"wordcloud\">Word cloud</aha-radio>\n</div>\n\n<!-- variant=\"button\" → a segmented control of connected pills (size=\"small\" for a compact row). -->\n<div role=\"radiogroup\" aria-label=\"View\" style=\"display:flex\">\n  <aha-radio name=\"view\" value=\"grid\" variant=\"button\" checked>Grid</aha-radio>\n  <aha-radio name=\"view\" value=\"list\" variant=\"button\">List</aha-radio>\n  <aha-radio name=\"view\" value=\"board\" variant=\"button\">Board</aha-radio>\n</div>\n\n<!-- variant=\"card\" → the whole bordered card is the target; `description` adds a secondary line.\n     The selected card gets a brand border + a subtle brand-tint fill. -->\n<div role=\"radiogroup\" aria-label=\"Plan\" style=\"display:flex;flex-direction:column;gap:10px;max-width:360px\">\n  <aha-radio name=\"plan\" value=\"pro\" variant=\"card\" checked\n    description=\"Best for teams — unlimited slides and live results.\">Pro</aha-radio>\n  <aha-radio name=\"plan\" value=\"essential\" variant=\"card\"\n    description=\"For solo creators getting started.\">Essential</aha-radio>\n</div>\n\n<script>\n  // Plain DOM — no framework. Read e.detail.value off the composed CustomEvent.\n  document.querySelectorAll('aha-radio[name=\"mode\"]').forEach((r) => {\n    r.addEventListener('change', (e) => console.log('mode:', e.detail.value));\n  });\n</script>\n",
    "react": "import '@ahaslides-product/design/aha-radio';   // registers <aha-radio>\nimport { useRef, useEffect } from 'react';\n\n// React 18 needs a thin wrapper; React 19 can use <aha-radio> directly.\nfunction AhaRadio({ checked, value, name, disabled, variant, size, description, onChange, children }) {\n  const ref = useRef();\n  useEffect(() => {\n    const el = ref.current;\n    el.checked = !!checked;\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 <aha-radio ref={ref} name={name} value={value} variant={variant} size={size} description={description}>{children}</aha-radio>;\n}\n\n// usage — one `name` makes the set mutually exclusive; wrap in role=\"radiogroup\"\n<div role=\"radiogroup\" aria-label=\"Activity mode\">\n  <AhaRadio name=\"mode\" value=\"poll\" checked={mode === 'poll'} onChange={setMode}>Poll</AhaRadio>\n  <AhaRadio name=\"mode\" value=\"quiz\" checked={mode === 'quiz'} onChange={setMode}>Quiz</AhaRadio>\n</div>\n\n// segmented button group — variant=\"button\" (add size=\"small\" for a compact row)\n<div role=\"radiogroup\" aria-label=\"View\" style={{ display: 'flex' }}>\n  <AhaRadio name=\"view\" value=\"grid\" variant=\"button\" checked={view === 'grid'} onChange={setView}>Grid</AhaRadio>\n  <AhaRadio name=\"view\" value=\"list\" variant=\"button\" checked={view === 'list'} onChange={setView}>List</AhaRadio>\n</div>\n\n// card group — variant=\"card\"; the whole tile is the target, `description` adds a secondary line\n<div role=\"radiogroup\" aria-label=\"Plan\" style={{ display: 'flex', flexDirection: 'column', gap: 10, maxWidth: 360 }}>\n  <AhaRadio name=\"plan\" value=\"pro\" variant=\"card\" checked={plan === 'pro'} onChange={setPlan}\n    description=\"Best for teams — unlimited slides and live results.\">Pro</AhaRadio>\n  <AhaRadio name=\"plan\" value=\"essential\" variant=\"card\" checked={plan === 'essential'} onChange={setPlan}\n    description=\"For solo creators getting started.\">Essential</AhaRadio>\n</div>\n",
    "vue": "// main.ts — register the element + mark aha-* as custom elements\nimport '@ahaslides-product/design/aha-radio';\napp.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');\n\n// Component.vue\n<script setup>\nimport { ref } from 'vue';\nconst mode = ref('poll');\nconst view = ref('grid');\nconst plan = ref('pro');\n</script>\n\n<template>\n  <div role=\"radiogroup\" aria-label=\"Activity mode\">\n    <aha-radio\n      name=\"mode\" value=\"poll\"\n      :checked.prop=\"mode === 'poll'\"\n      @change=\"mode = $event.detail.value\"\n    >Poll</aha-radio>\n    <aha-radio\n      name=\"mode\" value=\"quiz\"\n      :checked.prop=\"mode === 'quiz'\"\n      @change=\"mode = $event.detail.value\"\n    >Quiz</aha-radio>\n  </div>\n\n  <!-- segmented button group — variant=\"button\" (add size=\"small\" for a compact row) -->\n  <div role=\"radiogroup\" aria-label=\"View\" style=\"display:flex\">\n    <aha-radio name=\"view\" value=\"grid\" variant=\"button\" :checked.prop=\"view === 'grid'\" @change=\"view = $event.detail.value\">Grid</aha-radio>\n    <aha-radio name=\"view\" value=\"list\" variant=\"button\" :checked.prop=\"view === 'list'\" @change=\"view = $event.detail.value\">List</aha-radio>\n  </div>\n\n  <!-- card group — variant=\"card\"; the whole tile is the target, description adds a secondary line -->\n  <div role=\"radiogroup\" aria-label=\"Plan\" style=\"display:flex;flex-direction:column;gap:10px;max-width:360px\">\n    <aha-radio name=\"plan\" value=\"pro\" variant=\"card\" description=\"Best for teams — unlimited slides and live results.\"\n      :checked.prop=\"plan === 'pro'\" @change=\"plan = $event.detail.value\">Pro</aha-radio>\n    <aha-radio name=\"plan\" value=\"essential\" variant=\"card\" description=\"For solo creators getting started.\"\n      :checked.prop=\"plan === 'essential'\" @change=\"plan = $event.detail.value\">Essential</aha-radio>\n  </div>\n</template>\n"
  }
}
