{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gummy-radio-group",
  "type": "registry:ui",
  "title": "Gummy Radio Group",
  "description": "Native fieldset and radio items with controlled state, arrow navigation, RTL, validation, and read-only behavior.",
  "registryDependencies": [
    "https://gummyui.dev/r/gummy-form-styles.json"
  ],
  "files": [
    {
      "path": "components/ui/GummyRadioGroup.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\ntype GummyRadioGroupContextValue = {\n  name: string;\n  value: string | undefined;\n  describedBy: string | undefined;\n  required: boolean;\n  disabled: boolean;\n  readOnly: boolean;\n  selectValue: (value: string) => void;\n};\n\nconst GummyRadioGroupContext =\n  React.createContext<GummyRadioGroupContextValue | null>(null);\n\nexport type GummyRadioGroupProps = Omit<\n  React.FieldsetHTMLAttributes<HTMLFieldSetElement>,\n  \"onChange\"\n> & {\n  label: React.ReactNode;\n  description?: React.ReactNode;\n  errorMessage?: React.ReactNode;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  orientation?: \"horizontal\" | \"vertical\";\n  readOnly?: boolean;\n  required?: boolean;\n};\n\nexport type GummyRadioItemProps = Omit<\n  React.InputHTMLAttributes<HTMLInputElement>,\n  \"checked\" | \"defaultChecked\" | \"name\" | \"type\"\n> & {\n  value: string;\n  label: React.ReactNode;\n  description?: React.ReactNode;\n};\n\nfunction joinClassNames(...values: Array<string | false | null | undefined>) {\n  return values.filter(Boolean).join(\" \");\n}\n\nfunction joinIds(...values: Array<string | undefined>) {\n  return values.filter(Boolean).join(\" \") || undefined;\n}\n\nexport const GummyRadioGroup = React.forwardRef<\n  HTMLFieldSetElement,\n  GummyRadioGroupProps\n>(function GummyRadioGroup(\n  {\n    label,\n    description,\n    errorMessage,\n    value: controlledValue,\n    defaultValue,\n    onValueChange,\n    orientation = \"vertical\",\n    readOnly = false,\n    required = false,\n    disabled = false,\n    name: providedName,\n    className,\n    children,\n    ...fieldsetProps\n  },\n  ref,\n) {\n  const generatedId = React.useId().replace(/:/g, \"\");\n  const name = providedName ?? `gummy-radio-${generatedId}`;\n  const descriptionId = description ? `${name}-description` : undefined;\n  const errorId = errorMessage ? `${name}-error` : undefined;\n  const readOnlyId = readOnly ? `${name}-readonly` : undefined;\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);\n  const value =\n    controlledValue === undefined ? uncontrolledValue : controlledValue;\n\n  const selectValue = React.useCallback(\n    (nextValue: string) => {\n      if (disabled || readOnly) return;\n      if (controlledValue === undefined) setUncontrolledValue(nextValue);\n      onValueChange?.(nextValue);\n    },\n    [controlledValue, disabled, onValueChange, readOnly],\n  );\n\n  const contextValue = React.useMemo<GummyRadioGroupContextValue>(\n    () => ({\n      name,\n      value,\n      describedBy: joinIds(descriptionId, errorId, readOnlyId),\n      required,\n      disabled,\n      readOnly,\n      selectValue,\n    }),\n    [\n      descriptionId,\n      disabled,\n      errorId,\n      name,\n      readOnlyId,\n      readOnly,\n      required,\n      selectValue,\n      value,\n    ],\n  );\n\n  return (\n    <GummyRadioGroupContext.Provider value={contextValue}>\n      <fieldset\n        {...fieldsetProps}\n        ref={ref}\n        className={joinClassNames(\"gummy-radio-group\", className)}\n        disabled={disabled}\n        aria-describedby={joinIds(descriptionId, errorId, readOnlyId)}\n        aria-invalid={errorMessage ? true : undefined}\n        data-orientation={orientation}\n        data-disabled={disabled || undefined}\n        data-read-only={readOnly || undefined}\n        data-invalid={errorMessage || undefined}\n      >\n        <legend className=\"gummy-radio-group__legend\">\n          <span>{label}</span>\n          {required ? <span aria-hidden=\"true\">Required</span> : null}\n          {readOnly ? <span aria-hidden=\"true\">Read only</span> : null}\n        </legend>\n        {description ? (\n          <p className=\"gummy-radio-group__description\" id={descriptionId}>\n            {description}\n          </p>\n        ) : null}\n        {readOnlyId ? (\n          <span className=\"gummy-sr-only\" id={readOnlyId}>\n            Selection is read only and cannot be changed.\n          </span>\n        ) : null}\n        <div className=\"gummy-radio-group__items\">{children}</div>\n        {errorMessage ? (\n          <p className=\"gummy-radio-group__error\" id={errorId} role=\"alert\">\n            <span className=\"gummy-form-message-mark\" aria-hidden=\"true\" />\n            <span>{errorMessage}</span>\n          </p>\n        ) : null}\n      </fieldset>\n    </GummyRadioGroupContext.Provider>\n  );\n});\n\nGummyRadioGroup.displayName = \"GummyRadioGroup\";\n\nexport const GummyRadioItem = React.forwardRef<\n  HTMLInputElement,\n  GummyRadioItemProps\n>(function GummyRadioItem(\n  {\n    value,\n    label,\n    description,\n    disabled: itemDisabled = false,\n    required: itemRequired,\n    id: providedId,\n    className,\n    onChange,\n    onClick,\n    onKeyDown,\n    \"aria-describedby\": ariaDescribedBy,\n    ...inputProps\n  },\n  ref,\n) {\n  const context = React.useContext(GummyRadioGroupContext);\n  if (!context) {\n    throw new Error(\"GummyRadioItem must be used inside GummyRadioGroup.\");\n  }\n  const generatedId = React.useId().replace(/:/g, \"\");\n  const id = providedId ?? `gummy-radio-item-${generatedId}`;\n  const titleId = `${id}-label`;\n  const descriptionId = description ? `${id}-description` : undefined;\n  const disabled = context.disabled || itemDisabled;\n\n  function moveSelection(\n    event: React.KeyboardEvent<HTMLInputElement>,\n    position: \"next\" | \"previous\" | \"first\" | \"last\",\n  ) {\n    const fieldset = event.currentTarget.closest(\"fieldset\");\n    if (!fieldset) return;\n    const inputs = Array.from(\n      fieldset.querySelectorAll<HTMLInputElement>(\n        'input[type=\"radio\"]:not(:disabled)',\n      ),\n    );\n    const currentIndex = inputs.indexOf(event.currentTarget);\n    if (currentIndex < 0 || inputs.length === 0) return;\n    const targetIndex =\n      position === \"first\"\n        ? 0\n        : position === \"last\"\n          ? inputs.length - 1\n          : position === \"next\"\n            ? (currentIndex + 1) % inputs.length\n            : (currentIndex - 1 + inputs.length) % inputs.length;\n    inputs[targetIndex]?.focus();\n    inputs[targetIndex]?.click();\n  }\n\n  return (\n    <label\n      className=\"gummy-radio-item\"\n      data-disabled={disabled || undefined}\n      data-read-only={context.readOnly || undefined}\n      htmlFor={id}\n    >\n      <span className=\"gummy-radio-item__target\">\n        <input\n          {...inputProps}\n          ref={ref}\n          id={id}\n          type=\"radio\"\n          className={joinClassNames(\"gummy-radio-item__input\", className)}\n          name={context.name}\n          value={value}\n          checked={context.value === value}\n          disabled={disabled}\n          required={itemRequired ?? context.required}\n          aria-labelledby={titleId}\n          aria-describedby={joinIds(\n            ariaDescribedBy,\n            context.describedBy,\n            descriptionId,\n          )}\n          onClick={(event) => {\n            if (context.readOnly) event.preventDefault();\n            onClick?.(event);\n          }}\n          onKeyDown={(event) => {\n            if (\n              context.readOnly &&\n              [\" \", \"Enter\", \"ArrowLeft\", \"ArrowRight\", \"ArrowUp\", \"ArrowDown\"].includes(\n                event.key,\n              )\n            ) {\n              event.preventDefault();\n            } else if (!context.readOnly) {\n              const isRtl =\n                getComputedStyle(event.currentTarget).direction === \"rtl\";\n              const position =\n                event.key === \"ArrowDown\"\n                  ? \"next\"\n                  : event.key === \"ArrowUp\"\n                    ? \"previous\"\n                    : event.key === \"ArrowRight\"\n                      ? isRtl\n                        ? \"previous\"\n                        : \"next\"\n                      : event.key === \"ArrowLeft\"\n                        ? isRtl\n                          ? \"next\"\n                          : \"previous\"\n                        : event.key === \"Home\"\n                          ? \"first\"\n                          : event.key === \"End\"\n                            ? \"last\"\n                            : null;\n              if (position) {\n                event.preventDefault();\n                moveSelection(event, position);\n              }\n            }\n            onKeyDown?.(event);\n          }}\n          onChange={(event) => {\n            if (event.currentTarget.checked) context.selectValue(value);\n            onChange?.(event);\n          }}\n        />\n        <span className=\"gummy-radio-item__indicator\" aria-hidden=\"true\">\n          <span className=\"gummy-radio-item__pool\" />\n          <span className=\"gummy-radio-item__dot\" />\n        </span>\n      </span>\n      <span className=\"gummy-radio-item__copy\">\n        <span className=\"gummy-radio-item__title\" id={titleId}>{label}</span>\n        {description ? (\n          <span className=\"gummy-radio-item__description\" id={descriptionId}>\n            {description}\n          </span>\n        ) : null}\n      </span>\n    </label>\n  );\n});\n\nGummyRadioItem.displayName = \"GummyRadioItem\";\n"
    }
  ]
}
