{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "radio-group",
  "title": "Radio Group",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/radio-group/RadioGroup.vue",
      "content": "<script setup lang=\"ts\">\nimport type { RadioGroupRootProps } from 'reka-ui'\nimport type { HTMLAttributes } from 'vue'\nimport { computed, getCurrentInstance, provide } from 'vue'\nimport { reactiveOmit } from '@vueuse/core'\nimport { RadioGroupRoot, useForwardPropsEmits } from 'reka-ui'\nimport { cn } from '@/lib/utils'\n\nexport type RadioOption = string | { label: string; value: string; disabled?: boolean }\n\n// CLAUDE.md mandate: avoid `interface extends ReakUiX` (SFC compiler bails\n// in Vue 3.5+ because reka-ui has no exports.types). The intersection form\n// `defineProps<External & Extra>()` delegates type extraction to TS.\nexport type RadioGroupProps = Omit<RadioGroupRootProps, 'defaultValue' | 'modelValue'> & {\n  class?: HTMLAttributes['class']\n  /** The controlled value of the radio items to check. Can be binded with v-model. */\n  modelValue?: any\n  /** The value of the radio items that should be checked when initially rendered. */\n  defaultValue?: any\n  /** When `true`, prevents the user from interacting with the radio group */\n  disabled?: boolean\n  /** The orientation of the radio items */\n  orientation?: 'horizontal' | 'vertical'\n  /** When `true`, keyboard navigation will loop from last item to first, and vice versa */\n  loop?: boolean\n  /** Label for the radio group */\n  label?: string\n  /** Hint text for the radio group */\n  hint?: string\n  /** Error messages to display */\n  errorMessages?: string | string[]\n  /** Whether to show error state */\n  error?: boolean\n  /** Density of the radio items */\n  density?: 'compact' | 'default' | 'comfortable'\n  /** Whether the radio group appears flat */\n  flat?: boolean\n  /** Whether to show a border around the group */\n  bordered?: boolean\n  /** The reading direction */\n  dir?: 'ltr' | 'rtl'\n  /** Array of options to render automatically */\n  options?: RadioOption[]\n  /** Size of button-style radios */\n  size?: 'small' | 'middle' | 'large'\n  /** Type of options to render */\n  optionType?: 'default' | 'button'\n  /** Visual variant for button-style radios */\n  buttonVariant?: 'outline' | 'solid'\n}\n\nconst props = withDefaults(defineProps<RadioGroupProps>(), {\n  orientation: 'vertical',\n  density: 'default',\n  flat: false,\n  bordered: false,\n  loop: true,\n  disabled: false,\n  size: 'middle',\n  optionType: 'default',\n  buttonVariant: 'outline',\n})\n\nconst emits = defineEmits<{\n  'update:modelValue': [value: any]\n}>()\n\nprovide('radioGroup', {\n  disabled: props.disabled,\n  size: props.size,\n  optionType: props.optionType,\n  buttonVariant: props.buttonVariant,\n  orientation: props.orientation,\n})\n\nconst delegatedProps = reactiveOmit(\n  props,\n  'class',\n  'label',\n  'hint',\n  'errorMessages',\n  'error',\n  'density',\n  'flat',\n  'bordered',\n  'defaultValue',\n  'modelValue',\n  'options',\n  'size',\n  'optionType',\n  'buttonVariant',\n)\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\n\nconst instance = getCurrentInstance()\nconst isControlled = computed(() => Boolean(instance?.vnode?.props?.['onUpdate:modelValue']))\nconst userPassedModelValue = computed(() => {\n  const raw = instance?.vnode?.props\n  return Boolean(raw && ('modelValue' in raw || 'model-value' in raw))\n})\n\nconst radioStateBindings = computed(() => {\n  if (isControlled.value) return { 'model-value': props.modelValue }\n  // Uncontrolled — seed default-value from explicit modelValue or defaultValue prop.\n  if (props.defaultValue !== undefined) return { 'default-value': props.defaultValue }\n  if (userPassedModelValue.value) return { 'default-value': props.modelValue }\n  return {}\n})\n\n// Density classes\nconst densityClasses = {\n  compact: 'gap-1',\n  default: 'gap-3',\n  comfortable: 'gap-4',\n}\n\n// Build error state\nconst hasError = computed(() => {\n  if (props.error) return true\n  if (\n    props.errorMessages &&\n    (typeof props.errorMessages === 'string' ? props.errorMessages : props.errorMessages.length > 0)\n  )\n    return true\n  return false\n})\n\nfunction normalizeOption(option: RadioOption): { label: string; value: string; disabled?: boolean } {\n  if (typeof option === 'string') {\n    return { label: option, value: option }\n  }\n  return option\n}\n</script>\n\n<template>\n  <!--\n    `props.class` is forwarded ONLY to RadioGroupRoot below (per shadcn\n    convention). Applying it on the outer wrapper as well caused grid-*\n    utilities to fight the wrapper's `flex flex-col`, so consumers had\n    to fall back to column-count hacks. Forwarding to one element keeps\n    layout intent unambiguous.\n  -->\n  <div class=\"flex flex-col gap-2\">\n    <label v-if=\"label\" class=\"text-sm font-medium\">\n      {{ label }}\n    </label>\n\n    <p v-if=\"hint && !hasError\" class=\"text-muted-foreground text-xs\">\n      {{ hint }}\n    </p>\n\n    <RadioGroupRoot\n      v-slot=\"slotProps\"\n      v-bind=\"{ 'data-slot': 'radio-group', ...forwarded, ...radioStateBindings }\"\n      :class=\"\n        cn(\n          'grid gap-3',\n          orientation === 'horizontal' && 'flex flex-row items-center gap-4',\n          optionType === 'button' && orientation === 'horizontal' && 'flex flex-row items-stretch gap-0',\n          optionType === 'button' && orientation === 'vertical' && 'flex flex-col items-stretch gap-0',\n          optionType !== 'button' && densityClasses[density],\n          bordered && 'rounded-lg border p-4',\n          props.class,\n        )\n      \"\n      @update:model-value=\"(val: any) => emits('update:modelValue', val)\"\n    >\n      <template v-if=\"options && options.length > 0\">\n        <template v-if=\"optionType === 'button'\">\n          <RadioButton\n            v-for=\"option in options\"\n            :key=\"normalizeOption(option).value\"\n            :value=\"normalizeOption(option).value\"\n            :disabled=\"normalizeOption(option).disabled\"\n            :label=\"normalizeOption(option).label\"\n          />\n        </template>\n        <template v-else>\n          <div v-for=\"option in options\" :key=\"normalizeOption(option).value\" class=\"flex items-center gap-2\">\n            <RadioGroupItem\n              :id=\"normalizeOption(option).value\"\n              :value=\"normalizeOption(option).value\"\n              :disabled=\"normalizeOption(option).disabled\"\n            />\n            <label\n              :for=\"normalizeOption(option).value\"\n              class=\"cursor-pointer text-sm font-medium select-none\"\n              :class=\"normalizeOption(option).disabled && 'cursor-not-allowed opacity-50'\"\n            >\n              {{ normalizeOption(option).label }}\n            </label>\n          </div>\n        </template>\n      </template>\n\n      <slot v-bind=\"slotProps\" />\n    </RadioGroupRoot>\n\n    <div v-if=\"hasError\" class=\"flex flex-col gap-0.5\">\n      <p v-if=\"typeof errorMessages === 'string'\" class=\"text-destructive text-xs\">\n        {{ errorMessages }}\n      </p>\n      <template v-else>\n        <p v-for=\"(msg, i) in errorMessages\" :key=\"i\" class=\"text-destructive text-xs\">\n          {{ msg }}\n        </p>\n      </template>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/radio-group/RadioGroup.vue"
    },
    {
      "path": "packages/registry-vue/components/radio-group/RadioGroupItem.vue",
      "content": "<script setup lang=\"ts\">\nimport type { RadioGroupItemProps } from 'reka-ui'\nimport type { HTMLAttributes } from 'vue'\nimport { computed, inject } from 'vue'\nimport { reactiveOmit } from '@vueuse/core'\nimport { Circle } from 'lucide-vue-next'\nimport { RadioGroupIndicator, RadioGroupItem, useForwardProps } from 'reka-ui'\nimport { cn } from '@/lib/utils'\n\n// CLAUDE.md mandate: avoid `interface extends ReakUiX` (SFC compiler bails\n// in Vue 3.5+ because reka-ui has no exports.types). Intersection form below.\nexport type RadioGroupItemPropsExtended = Omit<RadioGroupItemProps, 'defaultChecked'> & {\n  class?: HTMLAttributes['class']\n  /** Size of the radio item */\n  size?: 'sm' | 'md' | 'lg'\n  /** Custom color for the checked state */\n  color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | string\n  /** Label text displayed next to the radio item */\n  label?: string\n  /** Hint text shown below the radio item */\n  hint?: string\n  /** Error message to display */\n  errorMessages?: string | string[]\n  /** Whether to show error state */\n  error?: boolean\n  /** Density of the radio item */\n  density?: 'compact' | 'default' | 'comfortable'\n  /** Label position - before or after the radio */\n  labelPosition?: 'before' | 'after'\n  /** Loading state */\n  loading?: boolean\n  /** Hide the indicator icon */\n  hideIcon?: boolean\n}\n\nconst props = withDefaults(defineProps<RadioGroupItemPropsExtended>(), {\n  size: 'md',\n  density: 'default',\n  color: 'primary',\n  labelPosition: 'after',\n  hideIcon: false,\n})\n\nconst delegatedProps = reactiveOmit(\n  props,\n  'class',\n  'size',\n  'color',\n  'label',\n  'hint',\n  'errorMessages',\n  'error',\n  'density',\n  'labelPosition',\n  'loading',\n  'hideIcon',\n)\n\nconst forwardedProps = useForwardProps(delegatedProps)\n\nconst groupContext = inject<{\n  disabled?: boolean\n  size?: 'small' | 'middle' | 'large'\n  optionType?: 'default' | 'button'\n  buttonVariant?: 'outline' | 'solid'\n  orientation?: 'horizontal' | 'vertical'\n}>('radioGroup', {})\n\nconst effectiveDisabled = computed(() => props.disabled ?? groupContext.disabled ?? false)\n\n// Size classes\nconst sizeClasses = {\n  sm: 'size-3.5',\n  md: 'size-4',\n  lg: 'size-5',\n}\n\nconst indicatorSizes = {\n  sm: 'size-1.5',\n  md: 'size-2',\n  lg: 'size-2.5',\n}\n\n// Color classes\nconst colorClasses: Record<string, string> = {\n  primary: 'data-[state=checked]:border-primary',\n  secondary: 'data-[state=checked]:border-secondary',\n  success: 'data-[state=checked]:border-[var(--success)] data-[state=checked]:text-[var(--success)]',\n  warning: 'data-[state=checked]:border-[var(--warning)] data-[state=checked]:text-[var(--warning)]',\n  error: 'data-[state=checked]:border-destructive data-[state=checked]:text-destructive',\n  info: 'data-[state=checked]:border-[var(--info)] data-[state=checked]:text-[var(--info)]',\n}\n\n// Density classes\nconst densityClasses = {\n  compact: 'gap-1',\n  default: 'gap-2',\n  comfortable: 'gap-3',\n}\n\n// Build error state\nconst hasError = computed(() => {\n  if (props.error) return true\n  if (\n    props.errorMessages &&\n    (typeof props.errorMessages === 'string' ? props.errorMessages : props.errorMessages.length > 0)\n  )\n    return true\n  return false\n})\n</script>\n\n<template>\n  <div class=\"flex items-start\" :class=\"[densityClasses[density]]\">\n    <label\n      v-if=\"label && labelPosition === 'before'\"\n      :for=\"id\"\n      class=\"mr-2 cursor-pointer text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n      :class=\"[hasError ? 'text-destructive' : '', effectiveDisabled && 'cursor-not-allowed opacity-50']\"\n    >\n      {{ label }}\n    </label>\n\n    <div class=\"flex items-center\">\n      <RadioGroupItem\n        v-bind=\"forwardedProps\"\n        :id=\"id\"\n        data-uipkge\n        data-slot=\"radio-group-item\"\n        :value=\"value\"\n        :disabled=\"effectiveDisabled\"\n        :class=\"\n          cn(\n            'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square shrink-0 rounded-full border shadow-sm transition-colors duration-200 outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',\n            sizeClasses[size],\n            colorClasses[color] || colorClasses.primary,\n            hasError && 'border-destructive',\n            props.class,\n          )\n        \"\n      >\n        <RadioGroupIndicator\n          data-uipkge\n          data-slot=\"radio-group-indicator\"\n          class=\"relative flex items-center justify-center\"\n        >\n          <slot>\n            <Circle\n              v-if=\"!hideIcon\"\n              :class=\"\n                cn(\n                  'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 fill-current text-current',\n                  indicatorSizes[size],\n                )\n              \"\n            />\n          </slot>\n        </RadioGroupIndicator>\n      </RadioGroupItem>\n\n      <label\n        v-if=\"label && labelPosition === 'after'\"\n        :for=\"id\"\n        class=\"ml-2 cursor-pointer text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n        :class=\"[hasError ? 'text-destructive' : '', effectiveDisabled && 'cursor-not-allowed opacity-50']\"\n      >\n        {{ label }}\n      </label>\n    </div>\n\n    <p v-if=\"hint && !hasError\" class=\"text-muted-foreground mt-1 ml-6 text-xs\">\n      {{ hint }}\n    </p>\n\n    <div v-if=\"hasError\" class=\"mt-1 ml-6 flex flex-col gap-0.5\">\n      <p v-if=\"typeof errorMessages === 'string'\" class=\"text-destructive text-xs\">\n        {{ errorMessages }}\n      </p>\n      <template v-else>\n        <p v-for=\"(msg, i) in errorMessages\" :key=\"i\" class=\"text-destructive text-xs\">\n          {{ msg }}\n        </p>\n      </template>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/radio-group/RadioGroupItem.vue"
    },
    {
      "path": "packages/registry-vue/components/radio-group/RadioButton.vue",
      "content": "<script setup lang=\"ts\">\nimport type { RadioGroupItemProps } from 'reka-ui'\nimport type { HTMLAttributes } from 'vue'\nimport { computed, inject } from 'vue'\nimport { reactiveOmit } from '@vueuse/core'\nimport { RadioGroupItem, useForwardProps } from 'reka-ui'\nimport { cn } from '@/lib/utils'\n\n// CLAUDE.md mandate: avoid `interface extends ReakUiX` (SFC compiler bails\n// in Vue 3.5+ because reka-ui has no exports.types). Intersection form below.\nexport type RadioButtonProps = Omit<RadioGroupItemProps, 'defaultChecked'> & {\n  class?: HTMLAttributes['class']\n  /** Size of the button radio */\n  size?: 'small' | 'middle' | 'large'\n  /** Visual variant */\n  variant?: 'outline' | 'solid'\n  /** Label text */\n  label?: string\n}\n\nconst props = withDefaults(defineProps<RadioButtonProps>(), {\n  size: undefined,\n  variant: undefined,\n})\n\nconst context = inject<{\n  disabled?: boolean\n  size?: 'small' | 'middle' | 'large'\n  optionType?: 'default' | 'button'\n  buttonVariant?: 'outline' | 'solid'\n  orientation?: 'horizontal' | 'vertical'\n}>('radioGroup', {})\n\nconst effectiveSize = computed(() => props.size ?? context.size ?? 'middle')\nconst effectiveVariant = computed(() => props.variant ?? context.buttonVariant ?? 'outline')\nconst effectiveDisabled = computed(() => props.disabled ?? context.disabled ?? false)\n\nconst delegatedProps = reactiveOmit(props, 'class', 'size', 'variant', 'label')\nconst forwardedProps = useForwardProps(delegatedProps)\n\nconst sizeClasses = {\n  small: 'h-7 px-2.5 text-xs',\n  middle: 'h-8 px-4 text-sm',\n  large: 'h-10 px-4.5 text-base',\n}\n\nconst variantClasses = {\n  outline: cn(\n    'border border-input bg-transparent text-foreground hover:text-foreground hover:bg-muted/50',\n    'data-[state=checked]:border-primary data-[state=checked]:text-primary',\n    'disabled:hover:bg-transparent',\n  ),\n  solid: cn(\n    'border border-input bg-transparent text-foreground hover:text-foreground hover:bg-muted/50',\n    'data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',\n    'disabled:hover:bg-transparent',\n  ),\n}\n\nconst groupClasses = computed(() => {\n  if (context.orientation === 'vertical') {\n    return 'rounded-md w-full justify-start'\n  }\n  return cn('rounded-none first:rounded-l-md last:rounded-r-md', 'border-l-0 first:border-l', '-ml-px first:ml-0')\n})\n</script>\n\n<template>\n  <RadioGroupItem\n    v-bind=\"forwardedProps\"\n    data-uipkge\n    data-slot=\"radio-button\"\n    :value=\"value\"\n    :disabled=\"effectiveDisabled\"\n    :class=\"\n      cn(\n        'inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap transition-colors duration-200',\n        'focus-visible:border-ring focus-visible:ring-ring/50 outline-none focus-visible:ring-[3px]',\n        'disabled:cursor-not-allowed disabled:opacity-50',\n        sizeClasses[effectiveSize],\n        variantClasses[effectiveVariant],\n        groupClasses,\n        props.class,\n      )\n    \"\n  >\n    <slot>\n      {{ label ?? value }}\n    </slot>\n  </RadioGroupItem>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/radio-group/RadioButton.vue"
    },
    {
      "path": "packages/registry-vue/components/radio-group/index.ts",
      "content": "export { default as RadioGroup } from './RadioGroup.vue'\nexport { default as RadioGroupItem } from './RadioGroupItem.vue'\nexport { default as RadioButton } from './RadioButton.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/radio-group/index.ts"
    }
  ],
  "dependencies": [
    "@vueuse/core",
    "lucide-vue-next",
    "reka-ui"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Single-selection group of radio inputs. Vertical or horizontal layout, optional descriptions per item, and full keyboard navigation. Pair with Form for validation messages.",
  "categories": [
    "form"
  ]
}