{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radio-group-list",
  "title": "Group-list radio",
  "description": "Styled radio: the Group-list set. 5 decorative radio variations (RowRadio, DividedRadio, GroupedRadio, InlineRadio, DenseRadio) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/radio-group-list.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* List radio-group family: 5 list layouts. The visual language is the same (a token\n   dot + a label); the difference is the LAYOUT: full-width rows, rows with a\n   divider, a bordered grouped block, a horizontal inline arrangement and a dense\n   list. role=\"radiogroup\" + role=\"radio\", roving tabindex, arrow-key navigation and\n   selection. Color comes ONLY from tokens, via alpha color-mix. A dot box under\n   24px carries an invisible touch area. Under reduced-motion the dot appears\n   instantly. Renders with no props too. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype RadioOption = { value: string; label: React.ReactNode }\n\ninterface RadioGroupProps {\n  className?: string\n  size?: StyledSize\n  options?: RadioOption[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (v: string) => void\n}\n\nconst DEFAULT_OPTIONS: RadioOption[] = [\n  { value: \"a\", label: \"Option A\" },\n  { value: \"b\", label: \"Option B\" },\n  { value: \"c\", label: \"Option C\" },\n]\n\nconst textSize: Record<StyledSize, string> = {\n  sm: \"text-xs\",\n  md: \"text-sm\",\n  lg: \"text-base\",\n  xl: \"text-lg\",\n}\n\nconst rowPad: Record<StyledSize, string> = {\n  sm: \"px-2.5 py-1.5\",\n  md: \"px-3 py-2\",\n  lg: \"px-4 py-2.5\",\n  xl: \"px-5 py-3\",\n}\n\nconst densePad: Record<StyledSize, string> = {\n  sm: \"px-2 py-0.5\",\n  md: \"px-2 py-1\",\n  lg: \"px-2.5 py-1.5\",\n  xl: \"px-3 py-1.5\",\n}\n\nconst ringSize: Record<StyledSize, string> = {\n  sm: \"size-4\",\n  md: \"size-4\",\n  lg: \"size-5\",\n  xl: \"size-5\",\n}\n\nconst dotSize: Record<StyledSize, string> = {\n  sm: \"size-1.5\",\n  md: \"size-2\",\n  lg: \"size-2.5\",\n  xl: \"size-2.5\",\n}\n\nfunction useRadioGroup(props: Pick<RadioGroupProps, \"value\" | \"defaultValue\" | \"onValueChange\" | \"options\">) {\n  const { value, defaultValue, onValueChange, options } = props\n  const opts = options && options.length > 0 ? options : DEFAULT_OPTIONS\n  const reduce = useReducedMotion()\n  const [internal, setInternal] = React.useState(defaultValue ?? opts[0]?.value)\n  const selected = value ?? internal\n  const select = React.useCallback(\n    (v: string) => {\n      if (value === undefined) setInternal(v)\n      onValueChange?.(v)\n    },\n    [value, onValueChange]\n  )\n  const spring = reduce\n    ? { duration: 0 }\n    : ({ type: \"spring\" as const, stiffness: 500, damping: 34 })\n  return { opts, selected, select, reduce, spring }\n}\n\n/* Arrow-key navigation + selection (together with the roving tabindex). */\nfunction useRadioKeys(opts: RadioOption[], select: (v: string) => void) {\n  const refs = React.useRef<(HTMLButtonElement | null)[]>([])\n  const setRef = (i: number) => (el: HTMLButtonElement | null) => {\n    refs.current[i] = el\n  }\n  const onKeyDown = (i: number) => (e: React.KeyboardEvent) => {\n    const last = opts.length - 1\n    let next = -1\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") next = i === last ? 0 : i + 1\n    else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") next = i === 0 ? last : i - 1\n    if (next < 0) return\n    e.preventDefault()\n    const target = opts[next]\n    if (!target) return\n    select(target.value)\n    refs.current[next]?.focus()\n  }\n  return { setRef, onKeyDown }\n}\n\nconst focusRing =\n  \"outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n\n/* Shared shell: the layout classes come from outside, the marking is the same in every variant. */\nfunction ListShell({\n  props,\n  rootClass,\n  rowClass,\n  activeClass,\n  inactiveClass = \"text-muted-foreground hover:text-foreground\",\n  pad,\n  gapClass = \"gap-2.5\",\n}: {\n  props: RadioGroupProps\n  rootClass: string\n  rowClass?: string\n  activeClass: string\n  inactiveClass?: string\n  pad: Record<StyledSize, string>\n  gapClass?: string\n}) {\n  const { className, size = \"md\", options, value, defaultValue, onValueChange } = props\n  const { opts, selected, select, spring } = useRadioGroup({ options, value, defaultValue, onValueChange })\n  const { setRef, onKeyDown } = useRadioKeys(opts, select)\n  const activeIndex = opts.findIndex((o) => o.value === selected)\n\n  return (\n    <div\n      data-slot=\"styled-radio-group\"\n      role=\"radiogroup\"\n      aria-label=\"Choose an option\"\n      className={cn(rootClass, className)}\n    >\n      {opts.map((opt, i) => {\n        const active = opt.value === selected\n        const roving = active || (activeIndex < 0 && i === 0)\n        return (\n          <button\n            key={opt.value}\n            ref={setRef(i)}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={active}\n            tabIndex={roving ? 0 : -1}\n            onKeyDown={onKeyDown(i)}\n            onClick={() => select(opt.value)}\n            data-slot=\"styled-radio-item\"\n            className={cn(\n              /* min-h-6: in the dense variant py-0.5/py-1 plus a leading-none line dropped the row to 20px (measured: 89x20) and neighbouring options came closer than 24px. The base height is pinned to 24 independently of the padding; the visual padding is preserved exactly. */\n              \"group flex min-h-6 cursor-pointer items-center text-left font-medium transition-colors\",\n              gapClass,\n              focusRing,\n              textSize[size],\n              pad[size],\n              rowClass,\n              active ? activeClass : inactiveClass\n            )}\n          >\n            <span\n              className={cn(\n                \"relative flex shrink-0 items-center justify-center rounded-full border transition-colors after:absolute after:-inset-1.5\",\n                ringSize[size],\n                active ? \"border-primary\" : \"border-field-border group-hover:border-[color-mix(in_oklab,var(--color-primary)_40%,transparent)]\"\n              )}\n            >\n              <motion.span\n                initial={false}\n                animate={{ scale: active ? 1 : 0 }}\n                transition={spring}\n                className={cn(\"rounded-full bg-primary\", dotSize[size])}\n              />\n            </span>\n            <span className=\"min-w-0 flex-1 truncate\">{opt.label}</span>\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\n/* Row: full-width, spaced rows; the selected row gets a light token fill. */\nexport function RowRadio(props: RadioGroupProps) {\n  return (\n    <ListShell\n      props={props}\n      rootClass=\"flex w-full flex-col gap-1\"\n      rowClass=\"w-full rounded-lg\"\n      pad={rowPad}\n      activeClass=\"bg-[color-mix(in_oklab,var(--color-primary)_10%,transparent)] text-foreground\"\n      inactiveClass=\"text-muted-foreground hover:bg-muted hover:text-foreground\"\n    />\n  )\n}\n\n/* Divided: satirlar arasi ince ayrac cizgisi. */\nexport function DividedRadio(props: RadioGroupProps) {\n  return (\n    <ListShell\n      props={props}\n      rootClass=\"flex w-full flex-col divide-y divide-border\"\n      rowClass=\"w-full\"\n      pad={rowPad}\n      activeClass=\"text-foreground\"\n    />\n  )\n}\n\n/* Grouped: a single bordered block; the rows are joined and the selected one takes a tint. */\nexport function GroupedRadio(props: RadioGroupProps) {\n  return (\n    <ListShell\n      props={props}\n      rootClass=\"flex w-full flex-col divide-y divide-border overflow-hidden rounded-xl border border-border bg-card\"\n      rowClass=\"w-full\"\n      pad={rowPad}\n      activeClass=\"bg-[color-mix(in_oklab,var(--color-primary)_12%,transparent)] text-foreground\"\n      inactiveClass=\"text-muted-foreground hover:bg-muted hover:text-foreground\"\n    />\n  )\n}\n\n/* Inline: a horizontal, inline arrangement; it wraps in narrow spaces. */\nexport function InlineRadio(props: RadioGroupProps) {\n  return (\n    <ListShell\n      props={props}\n      rootClass=\"flex flex-wrap items-center gap-x-4 gap-y-2\"\n      rowClass=\"rounded-md\"\n      gapClass=\"gap-2\"\n      pad={densePad}\n      activeClass=\"text-foreground\"\n    />\n  )\n}\n\n/* Dense: siki aralikli, kompakt liste. */\nexport function DenseRadio(props: RadioGroupProps) {\n  return (\n    <ListShell\n      props={props}\n      rootClass=\"flex w-full flex-col gap-0.5\"\n      rowClass=\"w-full rounded-md\"\n      gapClass=\"gap-2\"\n      pad={densePad}\n      activeClass=\"text-foreground\"\n      inactiveClass=\"text-muted-foreground hover:text-foreground\"\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/radio-group-list.tsx"
    }
  ],
  "categories": [
    "styled",
    "radio"
  ],
  "type": "registry:component"
}