{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "radio-group",
  "title": "Radio Group",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/radio-group/radio-group.component.ts",
      "content": "import {\n  ChangeDetectorRef,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnDestroy,\n  Output,\n  booleanAttribute,\n  forwardRef,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms'\nimport { cn } from '@/lib/utils'\n\nexport type RadioOption = string | { label: string; value: string; disabled?: boolean }\nexport type RadioOrientation = 'horizontal' | 'vertical'\nexport type RadioDensity = 'compact' | 'default' | 'comfortable'\n/** Button-style radio sizes (group `size`, RadioButton `size`). */\nexport type RadioButtonSize = 'small' | 'middle' | 'large'\nexport type RadioItemDotSize = 'sm' | 'md' | 'lg'\nexport type RadioButtonVariant = 'outline' | 'solid'\nexport type RadioColor = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | (string & {})\n\nexport interface NormalizedRadioOption {\n  label: string\n  value: string\n  disabled?: boolean\n}\n\nexport function normalizeRadioOption(option: RadioOption): NormalizedRadioOption {\n  if (typeof option === 'string') return { label: option, value: option }\n  return option\n}\n\nfunction optionalBoolean(v: unknown): boolean | undefined {\n  return v === undefined || v === null ? undefined : booleanAttribute(v)\n}\n\nfunction hasErrors(error: boolean, errorMessages: string | string[] | undefined): boolean {\n  return (\n    error || Boolean(errorMessages && (typeof errorMessages === 'string' ? errorMessages : errorMessages.length > 0))\n  )\n}\n\nfunction toList(errorMessages: string | string[] | undefined): string[] {\n  if (!errorMessages) return []\n  return typeof errorMessages === 'string' ? [errorMessages] : errorMessages\n}\n\nconst GROUP_DENSITY_CLASSES: Record<RadioDensity, string> = {\n  compact: 'gap-1',\n  default: 'gap-3',\n  comfortable: 'gap-4',\n}\n\nconst ITEM_DENSITY_CLASSES: Record<RadioDensity, string> = {\n  compact: 'gap-1',\n  default: 'gap-2',\n  comfortable: 'gap-3',\n}\n\nconst ITEM_SIZE_CLASSES: Record<RadioItemDotSize, string> = {\n  sm: 'size-3.5',\n  md: 'size-4',\n  lg: 'size-5',\n}\n\nconst INDICATOR_SIZES: Record<RadioItemDotSize, string> = {\n  sm: 'size-1.5',\n  md: 'size-2',\n  lg: 'size-2.5',\n}\n\nconst ITEM_COLOR_CLASSES: Record<string, string> = {\n  primary: 'data-[state=checked]:border-primary',\n  secondary: 'data-[state=checked]:border-secondary',\n  success: 'data-[state=checked]:border-success data-[state=checked]:text-success',\n  warning: 'data-[state=checked]:border-warning data-[state=checked]:text-warning',\n  error: 'data-[state=checked]:border-destructive data-[state=checked]:text-destructive',\n  info: 'data-[state=checked]:border-info data-[state=checked]:text-info',\n}\n\nconst BUTTON_SIZE_CLASSES: Record<RadioButtonSize, string> = {\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 BUTTON_VARIANT_CLASSES: Record<RadioButtonVariant, string> = {\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\n/** Radix RovingFocusGroup key -> focus intent. */\nconst KEY_TO_INTENT: Record<string, 'first' | 'last' | 'prev' | 'next'> = {\n  ArrowLeft: 'prev',\n  ArrowUp: 'prev',\n  ArrowRight: 'next',\n  ArrowDown: 'next',\n  PageUp: 'first',\n  Home: 'first',\n  PageDown: 'last',\n  End: 'last',\n}\nconst ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']\n\n/** What the group needs from each radio (RadioGroupItem / RadioButton). */\ninterface RadioItem {\n  readonly value: string\n  readonly isDisabled: boolean\n  readonly hostEl: HTMLElement\n  focusTarget(): HTMLElement | null\n}\n\n/**\n * Angular port of the React RadioGroupItem: a Radix radio `<button role=\"radio\">` with the\n * circle indicator, plus optional label (before / after), hint and error messages. Reads\n * checked / disabled state from the enclosing RadioGroup. `class` / `id` go to the button.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-radio-group-item, [ui-radio-group-item], ui-radio-item',\n  standalone: true,\n  host: {\n    // [attr.class], not [class]: [class] merges in the consumer's static class attribute, which\n    // belongs to the inner root only.\n    '[attr.class]': 'wrapperClass',\n    // id belongs to the button; keep a static id off the host so <label for> finds the button.\n    '[attr.id]': 'null',\n  },\n  template: `\n    <div class=\"flex items-center\">\n      @if (label && labelPosition === 'before') {\n        <label [attr.for]=\"id\" [class]=\"labelClass('mr-2')\">{{ label }}</label>\n      }\n      <button\n        type=\"button\"\n        role=\"radio\"\n        data-uipkge=\"\"\n        data-slot=\"radio-group-item\"\n        [attr.id]=\"id\"\n        [attr.value]=\"value\"\n        [attr.aria-checked]=\"checked\"\n        [attr.aria-busy]=\"loading || null\"\n        [attr.aria-invalid]=\"hasError || null\"\n        [attr.data-state]=\"checked ? 'checked' : 'unchecked'\"\n        [attr.data-disabled]=\"isDisabled ? '' : null\"\n        [attr.data-orientation]=\"group?.orientation\"\n        [attr.tabindex]=\"group ? group.tabIndexFor(this) : null\"\n        [disabled]=\"isDisabled\"\n        [class]=\"buttonClass\"\n        (click)=\"onClick()\"\n        (focus)=\"group?.onItemFocus(this)\"\n        (keydown)=\"onKeydown($event)\"\n      >\n        @if (checked) {\n          <span\n            data-uipkge=\"\"\n            data-slot=\"radio-group-indicator\"\n            data-state=\"checked\"\n            [attr.data-disabled]=\"isDisabled ? '' : null\"\n            class=\"relative flex items-center justify-center\"\n          >\n            @if (!hideIcon) {\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                width=\"24\"\n                height=\"24\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                stroke-width=\"2\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n                [attr.class]=\"indicatorClass\"\n                aria-hidden=\"true\"\n              >\n                <circle cx=\"12\" cy=\"12\" r=\"10\" />\n              </svg>\n            }\n          </span>\n        }\n      </button>\n      @if (label && labelPosition === 'after') {\n        <label [attr.for]=\"id\" [class]=\"labelClass('ml-2')\">{{ label }}</label>\n      }\n    </div>\n    @if (hint && !hasError) {\n      <p class=\"text-muted-foreground ml-6 text-xs\">{{ hint }}</p>\n    }\n    @if (hasError) {\n      <div class=\"ml-6 flex flex-col gap-0.5\">\n        @for (msg of errorList; track $index) {\n          <p class=\"text-destructive text-xs\">{{ msg }}</p>\n        }\n      </div>\n    }\n  `,\n})\nexport class UiRadioGroupItemComponent implements RadioItem, OnDestroy {\n  // The group class is declared below (it imports these parts); resolved at construction time.\n  readonly group: UiRadioGroupComponent | null = inject(\n    forwardRef(() => UiRadioGroupComponent),\n    { optional: true },\n  )\n  readonly hostEl: HTMLElement = inject(ElementRef).nativeElement\n\n  @Input() value = ''\n  @Input() id?: string\n  @Input({ transform: optionalBoolean }) disabled?: boolean\n  @Input() size: RadioItemDotSize = 'md'\n  @Input() color: RadioColor = 'primary'\n  @Input() label?: string\n  @Input() hint?: string\n  @Input() errorMessages?: string | string[]\n  @Input({ transform: booleanAttribute }) error = false\n  @Input() density: RadioDensity = 'default'\n  @Input() labelPosition: 'before' | 'after' = 'after'\n  @Input({ transform: booleanAttribute }) loading = false\n  @Input({ transform: booleanAttribute }) hideIcon = false\n  @Input('class') className?: string\n\n  constructor() {\n    this.group?.register(this)\n  }\n\n  ngOnDestroy(): void {\n    this.group?.unregister(this)\n  }\n\n  get checked(): boolean {\n    return this.group?.isChecked(this.value) ?? false\n  }\n\n  /** React: loading || (disabled ?? group.disabled); Radix also ORs the group's disabled. */\n  get isDisabled(): boolean {\n    return this.loading || (this.disabled ?? this.group?.disabled ?? false) || !!this.group?.disabled\n  }\n\n  get hasError(): boolean {\n    return hasErrors(this.error, this.errorMessages)\n  }\n\n  get errorList(): string[] {\n    return toList(this.errorMessages)\n  }\n\n  get wrapperClass(): string {\n    return cn('flex flex-col', ITEM_DENSITY_CLASSES[this.density])\n  }\n\n  get buttonClass(): string {\n    return 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      ITEM_SIZE_CLASSES[this.size],\n      ITEM_COLOR_CLASSES[this.color] || ITEM_COLOR_CLASSES['primary'],\n      this.hasError && 'border-destructive',\n      this.loading && 'opacity-50',\n      this.className,\n    )\n  }\n\n  get indicatorClass(): string {\n    return cn(\n      'lucide lucide-circle',\n      'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 fill-current text-current',\n      INDICATOR_SIZES[this.size],\n    )\n  }\n\n  labelClass(side: string): string {\n    return cn(\n      side,\n      cn(\n        'cursor-pointer text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',\n        this.hasError ? 'text-destructive' : '',\n        this.isDisabled && 'cursor-not-allowed opacity-50',\n      ),\n    )\n  }\n\n  focusTarget(): HTMLElement | null {\n    return this.hostEl.querySelector<HTMLElement>('button[role=\"radio\"]')\n  }\n\n  onClick(): void {\n    if (!this.isDisabled && !this.checked) this.group?.select(this.value)\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    this.group?.onItemKeydown(this, event)\n  }\n}\n\n/**\n * Angular port of the React RadioButton (Ant-style button radio). The host is the Radix\n * radio itself: use `<button ui-radio-button>` for React's exact DOM, or `<ui-radio-button>`\n * (focusable role=\"radio\", Space selects). Size / variant / disabled default to the group's.\n * Content: projected children, else `label`, else `value` (React `children ?? label ?? value`).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-radio-button, [ui-radio-button]',\n  standalone: true,\n  host: {\n    '[attr.type]': 'isNativeButton ? \"button\" : null',\n    '[attr.role]': '\"radio\"',\n    '[attr.aria-checked]': 'checked',\n    '[attr.aria-disabled]': '!isNativeButton && isDisabled ? \"true\" : null',\n    '[attr.data-state]': 'checked ? \"checked\" : \"unchecked\"',\n    '[attr.data-disabled]': 'isDisabled ? \"\" : null',\n    '[attr.data-orientation]': 'group?.orientation',\n    '[attr.disabled]': 'isNativeButton && isDisabled ? \"\" : null',\n    '[attr.value]': 'value',\n    '[attr.tabindex]': 'group ? group.tabIndexFor(this) : isNativeButton ? null : 0',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"radio-button\"',\n    '[class]': 'hostClass',\n    '(click)': 'onClick()',\n    '(focus)': 'group?.onItemFocus(this)',\n    '(keydown)': 'onKeydown($event)',\n  },\n  template: `<ng-content>{{ label ?? value }}</ng-content>`,\n})\nexport class UiRadioButtonComponent implements RadioItem, OnDestroy {\n  // The group class is declared below (it imports these parts); resolved at construction time.\n  readonly group: UiRadioGroupComponent | null = inject(\n    forwardRef(() => UiRadioGroupComponent),\n    { optional: true },\n  )\n  readonly hostEl: HTMLElement = inject(ElementRef).nativeElement\n  readonly isNativeButton = this.hostEl.tagName === 'BUTTON'\n\n  @Input() value = ''\n  @Input({ transform: optionalBoolean }) disabled?: boolean\n  @Input() size?: RadioButtonSize\n  @Input() variant?: RadioButtonVariant\n  @Input() label?: string\n  @Input('class') className?: string\n\n  constructor() {\n    this.group?.register(this)\n  }\n\n  ngOnDestroy(): void {\n    this.group?.unregister(this)\n  }\n\n  get checked(): boolean {\n    return this.group?.isChecked(this.value) ?? false\n  }\n\n  get isDisabled(): boolean {\n    return (this.disabled ?? this.group?.disabled ?? false) || !!this.group?.disabled\n  }\n\n  get hostClass(): string {\n    const size = this.size ?? this.group?.size ?? 'middle'\n    const variant = this.variant ?? this.group?.buttonVariant ?? 'outline'\n    const groupClasses =\n      this.group?.orientation === 'vertical'\n        ? 'rounded-md w-full justify-start'\n        : cn('rounded-none first:rounded-l-md last:rounded-r-md', 'border-l-0 first:border-l', '-ml-px first:ml-0')\n    return 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      BUTTON_SIZE_CLASSES[size],\n      BUTTON_VARIANT_CLASSES[variant],\n      groupClasses,\n      // A custom-element host never matches :disabled, so mirror the disabled look via data-disabled.\n      !this.isNativeButton && 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',\n      this.className,\n    )\n  }\n\n  focusTarget(): HTMLElement {\n    return this.hostEl\n  }\n\n  onClick(): void {\n    if (!this.isDisabled && !this.checked) this.group?.select(this.value)\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    // Native buttons turn Space into a click; the custom-element host needs it done here.\n    if (!this.isNativeButton && event.key === ' ') {\n      event.preventDefault()\n      this.onClick()\n      return\n    }\n    this.group?.onItemKeydown(this, event)\n  }\n}\n\n/**\n * Angular port of the React RadioGroup (Radix RadioGroup). The host is React's outer\n * `flex flex-col gap-2` wrapper (label, hint, errors); inside it the `role=\"radiogroup\"`\n * root holds `options` or projected RadioGroupItem / RadioButton parts, which read the\n * group's value / disabled / size / variant / orientation through DI.\n *\n * Model is React's `value` / `defaultValue` / `valueChange` (`[(value)]`) plus\n * NG_VALUE_ACCESSOR. Keyboard is Radix roving focus: arrow keys move focus and select\n * (Left/Right ignored when vertical, Up/Down when horizontal, rtl flips), Home / End /\n * PageUp / PageDown move focus only, `loop` wraps, disabled items are skipped, and only\n * the checked (or first enabled) radio is in the Tab order. `class` goes to the root.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-radio-group, [ui-radio-group]',\n  standalone: true,\n  exportAs: 'uiRadioGroup',\n  imports: [UiRadioGroupItemComponent, UiRadioButtonComponent],\n  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiRadioGroupComponent), multi: true }],\n  host: { '[attr.class]': '\"flex flex-col gap-2\"' },\n  template: `\n    @if (label) {\n      <label class=\"text-sm font-medium\">{{ label }}</label>\n    }\n    @if (hint && !hasError) {\n      <p class=\"text-muted-foreground text-xs\">{{ hint }}</p>\n    }\n    <div\n      role=\"radiogroup\"\n      data-slot=\"radio-group\"\n      [attr.aria-required]=\"required || null\"\n      [attr.aria-orientation]=\"orientation\"\n      [attr.data-orientation]=\"orientation\"\n      [attr.data-disabled]=\"disabled ? '' : null\"\n      [attr.dir]=\"dir\"\n      [class]=\"rootClass\"\n    >\n      @if (normalizedOptions.length > 0) {\n        @if (optionType === 'button') {\n          @for (opt of normalizedOptions; track opt.value) {\n            <button ui-radio-button [value]=\"opt.value\" [disabled]=\"opt.disabled\" [label]=\"opt.label\"></button>\n          }\n        } @else {\n          @for (opt of normalizedOptions; track opt.value) {\n            <div class=\"flex items-center gap-2\">\n              <ui-radio-group-item [id]=\"opt.value\" [value]=\"opt.value\" [disabled]=\"opt.disabled\" />\n              <label [attr.for]=\"opt.value\" [class]=\"optionLabelClass(opt)\">{{ opt.label }}</label>\n            </div>\n          }\n        }\n      } @else {\n        <ng-content />\n      }\n    </div>\n    @if (hasError) {\n      <div class=\"flex flex-col gap-0.5\">\n        @for (msg of errorList; track $index) {\n          <p class=\"text-destructive text-xs\">{{ msg }}</p>\n        }\n      </div>\n    }\n  `,\n})\nexport class UiRadioGroupComponent implements ControlValueAccessor {\n  // Zoneless-safe: form writes happen outside template events, so schedule a repaint.\n  private readonly cdr = inject(ChangeDetectorRef)\n  private readonly _valueProp = signal<string | undefined>(undefined)\n  private readonly _internal = signal<string | null>(null)\n  private readonly items: RadioItem[] = []\n  /** Radix RovingFocusGroup currentTabStopId: the last focused radio. */\n  private currentTabStop: RadioItem | null = null\n\n  /** Controlled value (React `value`). Reading it returns the resolved value. */\n  @Input()\n  set value(v: string | null | undefined) {\n    this._valueProp.set(v ?? undefined)\n  }\n  get value(): string | undefined {\n    return this._valueProp() ?? this._internal() ?? this.defaultValue\n  }\n  @Input() defaultValue?: string\n  @Output() valueChange = new EventEmitter<string>()\n\n  // Signal-backed: projected items live in the parent's view, so a form's disable() must\n  // reach them without a parent re-render (zoneless).\n  private readonly _disabled = signal(false)\n  @Input({ transform: booleanAttribute })\n  set disabled(v: boolean) {\n    this._disabled.set(v)\n  }\n  get disabled(): boolean {\n    return this._disabled()\n  }\n  @Input() orientation: RadioOrientation = 'vertical'\n  @Input({ transform: booleanAttribute }) loop = true\n  @Input() label?: string\n  @Input() hint?: string\n  @Input() errorMessages?: string | string[]\n  @Input({ transform: booleanAttribute }) error = false\n  @Input() density: RadioDensity = 'default'\n  @Input({ transform: booleanAttribute }) flat = false\n  @Input({ transform: booleanAttribute }) bordered = false\n  @Input() dir: 'ltr' | 'rtl' = 'ltr'\n  @Input() options?: RadioOption[]\n  @Input() size: RadioButtonSize = 'middle'\n  @Input() optionType: 'default' | 'button' = 'default'\n  @Input() buttonVariant: RadioButtonVariant = 'outline'\n  @Input({ transform: booleanAttribute }) required = false\n  @Input() name?: string\n  @Input('class') className?: string\n\n  private onChange: (v: string) => void = () => {}\n  private onTouched: () => void = () => {}\n\n  get normalizedOptions(): NormalizedRadioOption[] {\n    return (this.options ?? []).map(normalizeRadioOption)\n  }\n\n  get hasError(): boolean {\n    return hasErrors(this.error, this.errorMessages)\n  }\n\n  get errorList(): string[] {\n    return toList(this.errorMessages)\n  }\n\n  get rootClass(): string {\n    return cn(\n      'grid gap-3',\n      this.orientation === 'horizontal' && 'flex flex-row items-center gap-4',\n      this.optionType === 'button' && this.orientation === 'horizontal' && 'flex flex-row items-stretch gap-0',\n      this.optionType === 'button' && this.orientation === 'vertical' && 'flex flex-col items-stretch gap-0',\n      this.optionType !== 'button' && GROUP_DENSITY_CLASSES[this.density],\n      this.bordered && 'rounded-lg border p-4',\n      this.className,\n    )\n  }\n\n  optionLabelClass(opt: NormalizedRadioOption): string {\n    return cn('cursor-pointer text-sm font-medium select-none', opt.disabled && 'cursor-not-allowed opacity-50')\n  }\n\n  isChecked(value: string): boolean {\n    return this.value !== undefined && this.value === value\n  }\n\n  select(value: string): void {\n    if (this.disabled || value === this.value) return\n    this._internal.set(value)\n    this.onChange(value)\n    this.onTouched()\n    this.valueChange.emit(value)\n  }\n\n  // ---- roving focus (Radix RovingFocusGroup) ----\n\n  register(item: RadioItem): void {\n    this.items.push(item)\n  }\n\n  unregister(item: RadioItem): void {\n    const i = this.items.indexOf(item)\n    if (i >= 0) this.items.splice(i, 1)\n    if (this.currentTabStop === item) this.currentTabStop = null\n  }\n\n  private ordered(): RadioItem[] {\n    return [...this.items].sort((a, b) =>\n      a.hostEl.compareDocumentPosition(b.hostEl) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1,\n    )\n  }\n\n  /** 0 for the one radio in the Tab order (last focused, else checked, else first enabled). */\n  tabIndexFor(item: RadioItem): number {\n    if (item.isDisabled) return -1\n    const enabled = this.ordered().filter((i) => !i.isDisabled)\n    const stop =\n      (this.currentTabStop && enabled.includes(this.currentTabStop) ? this.currentTabStop : null) ??\n      enabled.find((i) => this.isChecked(i.value)) ??\n      enabled[0]\n    return stop === item ? 0 : -1\n  }\n\n  onItemFocus(item: RadioItem): void {\n    this.currentTabStop = item\n  }\n\n  onItemKeydown(item: RadioItem, event: KeyboardEvent): void {\n    // WAI-ARIA: radio groups don't activate items on Enter.\n    if (event.key === 'Enter') {\n      event.preventDefault()\n      return\n    }\n    if (event.target !== event.currentTarget) return\n    const key =\n      this.dir === 'rtl' && event.key === 'ArrowLeft'\n        ? 'ArrowRight'\n        : this.dir === 'rtl' && event.key === 'ArrowRight'\n          ? 'ArrowLeft'\n          : event.key\n    if (this.orientation === 'vertical' && (key === 'ArrowLeft' || key === 'ArrowRight')) return\n    if (this.orientation === 'horizontal' && (key === 'ArrowUp' || key === 'ArrowDown')) return\n    const intent = KEY_TO_INTENT[key]\n    if (!intent || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return\n    event.preventDefault()\n    let candidates = this.ordered().filter((i) => !i.isDisabled)\n    if (intent === 'last') candidates.reverse()\n    if (intent === 'prev' || intent === 'next') {\n      if (intent === 'prev') candidates.reverse()\n      const start = candidates.indexOf(item) + 1\n      candidates = this.loop\n        ? candidates.map((_, i) => candidates[(start + i) % candidates.length]!)\n        : candidates.slice(start)\n    }\n    const target = candidates[0]?.focusTarget()\n    if (!target) return\n    target.focus()\n    // Radix RadioGroupItem: focus that arrives from an arrow key also checks the radio.\n    if (ARROW_KEYS.includes(event.key)) target.click()\n  }\n\n  writeValue(v: string | null): void {\n    this._internal.set(v ?? null)\n    this.cdr.markForCheck()\n  }\n\n  registerOnChange(fn: (v: string) => void): void {\n    this.onChange = fn\n  }\n\n  registerOnTouched(fn: () => void): void {\n    this.onTouched = fn\n  }\n\n  setDisabledState(isDisabled: boolean): void {\n    this.disabled = isDisabled\n    this.cdr.markForCheck()\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/radio-group/radio-group.component.ts"
    },
    {
      "path": "packages/registry-angular/components/radio-group/index.ts",
      "content": "export {\n  UiRadioGroupComponent,\n  UiRadioGroupItemComponent,\n  UiRadioGroupItemComponent as UiRadioItemComponent,\n  UiRadioButtonComponent,\n  normalizeRadioOption,\n  type RadioOption,\n  type NormalizedRadioOption,\n  type RadioOrientation,\n  type RadioDensity,\n  type RadioButtonSize,\n  type RadioButtonVariant,\n  type RadioItemDotSize,\n  type RadioColor,\n} from './radio-group.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/radio-group/index.ts"
    }
  ],
  "dependencies": [
    "@angular/forms"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Single-selection group with Radix RadioGroup semantics: roving focus where arrow keys move and select, Home/End, loop, disabled items skipped. Vertical or horizontal layout, options array, label / hint / error parts, per-item label and hint, and Ant-style RadioButton (outline / solid, three sizes).",
  "categories": [
    "form"
  ]
}