{
  "name": "radio-buttons",
  "layer": "base",
  "type": "component",
  "title": "Radio button components",
  "description": "Free and open-source React radio button components built for modern applications and websites. These radio buttons are built using React Aria and styled with Tailwind CSS.",
  "files": [
    {
      "path": "components/base/radio-buttons/radio-buttons.tsx",
      "target": "components/base/radio-buttons/radio-buttons.tsx",
      "type": "component",
      "content": "\"use client\";\n\nimport { type ReactNode, type Ref, createContext, useContext } from \"react\";\nimport {\n    Radio as AriaRadio,\n    RadioGroup as AriaRadioGroup,\n    type RadioGroupProps as AriaRadioGroupProps,\n    type RadioProps as AriaRadioProps,\n} from \"react-aria-components\";\nimport { cx } from \"@/utils/cx\";\nimport { warnDomProps } from \"@/utils/warn-dom-props\";\n\nexport interface RadioGroupContextType {\n    size?: \"sm\" | \"md\";\n}\n\nconst RadioGroupContext = createContext<RadioGroupContextType | null>(null);\n\nexport interface RadioButtonBaseProps {\n    size?: \"sm\" | \"md\";\n    className?: string;\n    isFocusVisible?: boolean;\n    isSelected?: boolean;\n    isDisabled?: boolean;\n}\n\nexport const RadioButtonBase = ({ className, isFocusVisible, isSelected, isDisabled, size = \"sm\" }: RadioButtonBaseProps) => {\n    return (\n        <div\n            className={cx(\n                \"bg-primary ring-primary flex size-4 shrink-0 cursor-pointer appearance-none items-center justify-center rounded-full ring-1 ring-inset\",\n                size === \"md\" && \"size-5\",\n                isSelected && \"bg-brand-solid ring-brand-solid\",\n                isDisabled && \"cursor-not-allowed opacity-50\",\n                isDisabled && !isSelected && \"bg-tertiary\",\n                isFocusVisible && \"outline-focus-ring outline-2 outline-offset-2\",\n                className,\n            )}\n        >\n            <div className={cx(\"bg-fg-white transition-inherit-all size-1.5 rounded-full opacity-0\", size === \"md\" && \"size-2\", isSelected && \"opacity-100\")} />\n        </div>\n    );\n};\nRadioButtonBase.displayName = \"RadioButtonBase\";\n\ninterface RadioButtonProps extends AriaRadioProps {\n    size?: \"sm\" | \"md\";\n    label?: ReactNode;\n    hint?: ReactNode;\n    ref?: Ref<HTMLLabelElement>;\n}\n\nexport const RadioButton = ({ label, hint, className, size = \"sm\", ...ariaRadioProps }: RadioButtonProps) => {\n    const context = useContext(RadioGroupContext);\n\n    size = context?.size ?? size;\n\n    const sizes = {\n        sm: {\n            root: \"gap-2\",\n            textWrapper: \"\",\n            label: \"text-sm font-medium\",\n            hint: \"text-sm\",\n        },\n        md: {\n            root: \"gap-3\",\n            textWrapper: \"gap-0.5\",\n            label: \"text-md font-medium\",\n            hint: \"text-md\",\n        },\n    };\n\n    return (\n        <AriaRadio\n            {...ariaRadioProps}\n            className={(state) =>\n                cx(\n                    \"relative flex items-start\",\n                    state.isDisabled && \"cursor-not-allowed\",\n                    sizes[size].root,\n                    typeof className === \"function\" ? className(state) : className,\n                )\n            }\n        >\n            {({ isSelected, isDisabled, isFocusVisible }) => (\n                <>\n                    <RadioButtonBase\n                        size={size}\n                        isSelected={isSelected}\n                        isDisabled={isDisabled}\n                        isFocusVisible={isFocusVisible}\n                        className={label || hint ? \"mt-0.5\" : \"\"}\n                    />\n                    {(label || hint) && (\n                        <div className={cx(\"inline-flex flex-col\", sizes[size].textWrapper)}>\n                            {label && <p className={cx(\"text-secondary select-none\", sizes[size].label)}>{label}</p>}\n                            {hint && (\n                                <span role=\"presentation\" className={cx(\"text-tertiary\", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>\n                                    {hint}\n                                </span>\n                            )}\n                        </div>\n                    )}\n                </>\n            )}\n        </AriaRadio>\n    );\n};\nRadioButton.displayName = \"RadioButton\";\n\ninterface RadioGroupProps extends RadioGroupContextType, AriaRadioGroupProps {\n    children: ReactNode;\n    className?: string;\n}\n\nexport const RadioGroup = ({ children, className, size = \"sm\", ...props }: RadioGroupProps) => {\n    warnDomProps(\"RadioGroup\", props as Record<string, unknown>, { disabled: \"isDisabled\", required: \"isRequired\", readOnly: \"isReadOnly\" });\n\n    return (\n        <RadioGroupContext.Provider value={{ size }}>\n            <AriaRadioGroup {...props} className={cx(\"flex flex-col gap-4\", className)}>\n                {children}\n            </AriaRadioGroup>\n        </RadioGroupContext.Provider>\n    );\n};\n",
      "dependencies": [
        "react",
        "react-aria-components"
      ]
    }
  ],
  "registryDependencies": [
    "cx",
    "warn-dom-props"
  ],
  "optionalRegistryDependencies": [],
  "dependencies": [
    "react",
    "react-aria-components"
  ],
  "cssVars": [],
  "examples": [
    "avatar",
    "checkbox",
    "disabled",
    "disabled-individual-option",
    "icon-card",
    "icon-simple",
    "payment-icon",
    "radio-button",
    "radio-button-example",
    "sizes",
    "with-label",
    "with-label-and-hint"
  ],
  "docs": "/components/radio-buttons",
  "intent": "Let a user choose exactly one option from a small, always-visible set of mutually exclusive choices.",
  "avoid_when": [
    "more than ~6 options, or the list needs to scroll (use select)",
    "multiple selections are valid (use checkbox)"
  ],
  "composes_with": [
    "form",
    "input"
  ],
  "a11y_contract": [
    "built on React Aria RadioGroup/Radio: roving-tabindex arrow-key navigation and checked-state semantics come for free; the group itself still needs an aria-label or a visible label element since RadioGroup has no default accessible name"
  ],
  "requires_data": [
    "2+ options with visible text labels; a group-level label or aria-label"
  ],
  "token_contract": [
    "bg-brand-solid",
    "bg-fg-white",
    "bg-primary",
    "bg-tertiary",
    "text-secondary",
    "text-tertiary"
  ],
  "changelog": [
    {
      "version": "0.2.0",
      "changes": [
        "Field API and accessible-name fixes from the agent feedback map (docs/spec/feedback/2026-09-11-agent-feedback-map.md items 2.17, 2.20, 2.22, and the JSDoc half of 2.11), batched across `input`, `select`, `checkbox`, `radio-buttons`, `badges`, `toggle`, `tags`, `buttons/button`, `date-picker`, `page-headers`, and `foundations/featured-icon`:"
      ]
    }
  ],
  "platforms": [
    "react",
    "next"
  ]
}
