{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "b-radio-group",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["@base-ui/react", "motion"],
  "devDependencies": [],
  "files": [
    {
      "path": "b-radio-group.tsx",
      "content": "\"use client\";\n\nimport { Radio as RadioPrimitive } from \"@base-ui/react/radio\";\nimport { RadioGroup as RadioGroupPrimitive } from \"@base-ui/react/radio-group\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]\";\n\nconst ringTransition = {\n  type: \"spring\" as const,\n  stiffness: 400,\n  damping: 22,\n};\n\nconst instantTransition = { duration: 0 } as const;\n\nconst dotMotion = {\n  animate: { scale: 1, opacity: 1 },\n  exit: { scale: 0.95, opacity: 0 },\n  initial: { scale: 0.95, opacity: 0 },\n  transition: {\n    type: \"spring\" as const,\n    stiffness: 500,\n    damping: 25,\n    delay: 0.05,\n  },\n};\n\nconst textTransition = { duration: 0.2 };\n\nexport type RadioGroupOrientation = \"horizontal\" | \"vertical\";\n\nexport interface RadioOption {\n  value: string;\n  label: string;\n  description?: string;\n  disabled?: boolean;\n}\n\nexport interface RadioGroupProps {\n  options: RadioOption[];\n  defaultValue?: string;\n  value?: string;\n  onChange?: (value: string) => void;\n  className?: string;\n  disabled?: boolean;\n  form?: string;\n  invalid?: boolean;\n  label?: string;\n  labelClassName?: string;\n  name?: string;\n  orientation?: RadioGroupOrientation;\n  required?: boolean;\n  \"aria-describedby\"?: string;\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n}\n\nfunction getFirstSelectableValue(options: RadioOption[]) {\n  return options.find((option) => !option.disabled)?.value ?? options[0]?.value;\n}\n\nfunction isSelectableOption(options: RadioOption[], candidate: string) {\n  const option = options.find((item) => item.value === candidate);\n  return Boolean(option && !option.disabled);\n}\n\nfunction getValidSelection(\n  options: RadioOption[],\n  candidate?: string,\n  groupDisabled = false\n) {\n  const fallback = groupDisabled\n    ? options[0]?.value\n    : getFirstSelectableValue(options);\n\n  if (candidate === undefined) {\n    return fallback;\n  }\n\n  if (groupDisabled) {\n    return options.some((option) => option.value === candidate)\n      ? candidate\n      : fallback;\n  }\n\n  if (isSelectableOption(options, candidate)) {\n    return candidate;\n  }\n\n  return fallback;\n}\n\nfunction resolveGroupValue(\n  options: RadioOption[],\n  candidate: string | undefined,\n  allowFallback: boolean\n) {\n  if (\n    candidate !== undefined &&\n    options.some((option) => option.value === candidate)\n  ) {\n    return candidate;\n  }\n\n  return allowFallback ? getFirstSelectableValue(options) : undefined;\n}\n\nfunction getRingBorderColor(isSelected: boolean, invalid: boolean) {\n  if (isSelected) {\n    return \"var(--ic-primary)\";\n  }\n\n  if (invalid) {\n    return \"color-mix(in srgb, var(--ic-destructive) 45%, transparent)\";\n  }\n\n  return \"color-mix(in srgb, var(--ic-muted-foreground) 25%, transparent)\";\n}\n\nfunction getRadioRowMotionConfig(index: number, prefersReducedMotion: boolean) {\n  return {\n    dotMotionTransition: prefersReducedMotion\n      ? instantTransition\n      : dotMotion.transition,\n    entranceTransition: prefersReducedMotion\n      ? instantTransition\n      : { delay: index * 0.05, duration: 0.3 },\n    labelMotionTransition: prefersReducedMotion\n      ? instantTransition\n      : textTransition,\n    ringMotionTransition: prefersReducedMotion\n      ? instantTransition\n      : ringTransition,\n  };\n}\n\nfunction getRadioRowClassName({\n  invalid,\n  orientation,\n  rowDisabled,\n}: {\n  invalid: boolean;\n  orientation: RadioGroupOrientation;\n  rowDisabled: boolean;\n}) {\n  return cn(\n    \"relative flex touch-manipulation select-none items-center gap-3.5 rounded-lg py-3.5 text-left outline-none focus-visible:ring-2\",\n    orientation === \"horizontal\" ? \"min-w-[10rem] flex-1\" : \"w-full\",\n    invalid ? \"focus-visible:ring-destructive/40\" : \"focus-visible:ring-ring\",\n    rowDisabled && \"cursor-not-allowed opacity-50\"\n  );\n}\n\nfunction setRef<T>(ref: React.Ref<T> | undefined, value: T) {\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n\n  if (ref) {\n    (ref as React.MutableRefObject<T>).current = value;\n  }\n}\n\ntype BaseRadioRootRenderProps =\n  React.ButtonHTMLAttributes<HTMLButtonElement> & {\n    className?: string;\n    ref?: React.Ref<HTMLButtonElement>;\n  };\n\ntype RadioOptionIndicatorProps = {\n  invalid: boolean;\n  isSelected: boolean;\n  prefersReducedMotion: boolean;\n  ringMotionTransition: typeof ringTransition | typeof instantTransition;\n  dotMotionTransition: typeof dotMotion.transition | typeof instantTransition;\n};\n\nfunction RadioOptionIndicator({\n  invalid,\n  isSelected,\n  prefersReducedMotion,\n  ringMotionTransition,\n  dotMotionTransition,\n}: RadioOptionIndicatorProps) {\n  return (\n    <div\n      aria-hidden\n      className=\"relative z-10 flex h-5 w-5 shrink-0 items-center justify-center\"\n    >\n      <motion.div\n        animate={{\n          borderWidth: isSelected ? 6 : 2,\n          borderColor: getRingBorderColor(isSelected, invalid),\n        }}\n        className=\"absolute inset-0 rounded-full\"\n        style={{ borderStyle: \"solid\" }}\n        transition={ringMotionTransition}\n      />\n\n      <AnimatePresence>\n        {isSelected ? (\n          <motion.div\n            animate={dotMotion.animate}\n            className=\"absolute h-1.5 w-1.5 rounded-full bg-background\"\n            exit={prefersReducedMotion ? undefined : dotMotion.exit}\n            initial={prefersReducedMotion ? false : dotMotion.initial}\n            transition={dotMotionTransition}\n          />\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\ntype RadioOptionCopyProps = {\n  description?: string;\n  descriptionId?: string;\n  isSelected: boolean;\n  label: string;\n  labelId: string;\n  labelMotionTransition: typeof textTransition | typeof instantTransition;\n};\n\nfunction RadioOptionCopy({\n  description,\n  descriptionId,\n  isSelected,\n  label,\n  labelId,\n  labelMotionTransition,\n}: RadioOptionCopyProps) {\n  return (\n    <div className=\"relative z-10 flex flex-col gap-0.5\">\n      <motion.span\n        animate={{\n          color: isSelected\n            ? \"var(--ic-foreground)\"\n            : \"var(--ic-muted-foreground)\",\n          fontWeight: isSelected ? 500 : 400,\n        }}\n        className=\"text-[14px] leading-tight\"\n        id={labelId}\n        transition={labelMotionTransition}\n      >\n        {label}\n      </motion.span>\n\n      {description ? (\n        <motion.span\n          animate={{ opacity: isSelected ? 0.85 : 0.5 }}\n          className=\"text-muted-foreground/60 text-xs leading-snug\"\n          id={descriptionId}\n          transition={labelMotionTransition}\n        >\n          {description}\n        </motion.span>\n      ) : null}\n    </div>\n  );\n}\n\ntype BaseRadioOptionButtonProps = {\n  description?: string;\n  descriptionId?: string;\n  index: number;\n  invalid: boolean;\n  isSelected: boolean;\n  label: string;\n  labelId: string;\n  orientation: RadioGroupOrientation;\n  prefersReducedMotion: boolean;\n  rowDisabled: boolean;\n  rootProps: BaseRadioRootRenderProps;\n};\n\nfunction BaseRadioOptionButton({\n  description,\n  descriptionId,\n  index,\n  invalid,\n  isSelected,\n  label,\n  labelId,\n  orientation,\n  prefersReducedMotion,\n  rowDisabled,\n  rootProps,\n}: BaseRadioOptionButtonProps) {\n  const {\n    className: rootClassName,\n    onAnimationEnd: _onAnimationEnd,\n    onAnimationIteration: _onAnimationIteration,\n    onAnimationStart: _onAnimationStart,\n    onDrag: _onDrag,\n    onDragEnd: _onDragEnd,\n    onDragEnter: _onDragEnter,\n    onDragExit: _onDragExit,\n    onDragLeave: _onDragLeave,\n    onDragOver: _onDragOver,\n    onDragStart: _onDragStart,\n    onDrop: _onDrop,\n    ref: rootRef,\n    ...resolvedRootProps\n  } = rootProps;\n\n  const rowInteractive = !rowDisabled;\n  const motionConfig = getRadioRowMotionConfig(index, prefersReducedMotion);\n\n  return (\n    <motion.button\n      {...resolvedRootProps}\n      animate={{ opacity: 1, y: 0 }}\n      className={cn(\n        getRadioRowClassName({ invalid, orientation, rowDisabled }),\n        rootClassName\n      )}\n      data-slot=\"radio-row\"\n      initial={prefersReducedMotion ? false : { opacity: 0, y: 8 }}\n      ref={(node) => {\n        setRef(rootRef, node);\n      }}\n      transition={motionConfig.entranceTransition}\n      type=\"button\"\n      whileHover={\n        rowInteractive && !prefersReducedMotion ? { x: 2 } : undefined\n      }\n      whileTap={\n        rowInteractive && !prefersReducedMotion ? { scale: 0.98 } : undefined\n      }\n    >\n      <RadioOptionIndicator\n        dotMotionTransition={motionConfig.dotMotionTransition}\n        invalid={invalid}\n        isSelected={isSelected}\n        prefersReducedMotion={prefersReducedMotion}\n        ringMotionTransition={motionConfig.ringMotionTransition}\n      />\n\n      <RadioOptionCopy\n        description={description}\n        descriptionId={descriptionId}\n        isSelected={isSelected}\n        label={label}\n        labelId={labelId}\n        labelMotionTransition={motionConfig.labelMotionTransition}\n      />\n    </motion.button>\n  );\n}\n\nfunction RadioGroupLegend({\n  groupLabelId,\n  label,\n  labelClassName,\n  required,\n}: {\n  groupLabelId: string;\n  label: string;\n  labelClassName?: string;\n  required?: boolean;\n}) {\n  return (\n    <legend\n      className={cn(\n        \"mb-0 block w-full max-w-full px-0 pt-0 font-medium text-foreground text-sm [margin-inline:0] [padding-inline:0]\",\n        labelClassName\n      )}\n      id={groupLabelId}\n    >\n      {label}\n      {required ? (\n        <span aria-hidden className=\"text-destructive\">\n          {\" \"}\n          *\n        </span>\n      ) : null}\n    </legend>\n  );\n}\n\nconst RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(\n  (\n    {\n      \"aria-describedby\": ariaDescribedBy,\n      \"aria-label\": ariaLabel,\n      \"aria-labelledby\": ariaLabelledBy,\n      className,\n      defaultValue,\n      disabled = false,\n      form,\n      invalid = false,\n      label,\n      labelClassName,\n      name,\n      onChange,\n      options,\n      orientation = \"vertical\",\n      required,\n      value,\n    },\n    ref\n  ) => {\n    const generatedId = React.useId();\n    const prefersReducedMotion = Boolean(useReducedMotion());\n    const isControlled = value !== undefined;\n    const [uncontrolledSelected, setUncontrolledSelected] = React.useState<\n      string | undefined\n    >(() => getValidSelection(options, defaultValue, disabled));\n    const selected = isControlled ? value : uncontrolledSelected;\n    const groupName = name ?? `radio-group-${generatedId}`;\n    const groupLabelId = `${generatedId}-group-label`;\n    const resolvedAriaLabelledBy = label ? groupLabelId : ariaLabelledBy;\n    const resolvedAriaLabel =\n      label || resolvedAriaLabelledBy ? undefined : ariaLabel;\n    const primitiveValue = resolveGroupValue(options, selected, !isControlled);\n\n    React.useEffect(() => {\n      if (isControlled) {\n        return;\n      }\n\n      const normalized = getValidSelection(\n        options,\n        uncontrolledSelected,\n        disabled\n      );\n\n      if (normalized !== uncontrolledSelected) {\n        setUncontrolledSelected(normalized);\n      }\n    }, [disabled, isControlled, options, uncontrolledSelected]);\n\n    const handleSelect = React.useCallback(\n      (nextValue: string) => {\n        if (disabled || nextValue === selected) {\n          return;\n        }\n\n        if (!isSelectableOption(options, nextValue)) {\n          return;\n        }\n\n        if (!isControlled) {\n          setUncontrolledSelected(nextValue);\n        }\n\n        onChange?.(nextValue);\n      },\n      [disabled, isControlled, onChange, options, selected]\n    );\n\n    if (options.length === 0) {\n      return null;\n    }\n\n    return (\n      <fieldset\n        className=\"m-0 flex w-full min-w-0 flex-col gap-2 border-0 p-0 [padding-inline:0]\"\n        disabled={disabled || undefined}\n      >\n        {label ? (\n          <RadioGroupLegend\n            groupLabelId={groupLabelId}\n            label={label}\n            labelClassName={labelClassName}\n            required={required}\n          />\n        ) : null}\n        <div className=\"w-full min-w-0\">\n          <RadioGroupPrimitive\n            aria-describedby={ariaDescribedBy}\n            aria-invalid={invalid || undefined}\n            aria-label={resolvedAriaLabel}\n            aria-labelledby={resolvedAriaLabelledBy}\n            aria-orientation={orientation}\n            aria-required={required || undefined}\n            className={cn(\n              componentThemeClassName,\n              orientation === \"horizontal\"\n                ? \"flex flex-row flex-wrap gap-2\"\n                : \"flex flex-col gap-0.5\",\n              className\n            )}\n            disabled={disabled}\n            form={form}\n            name={groupName}\n            onValueChange={handleSelect}\n            ref={ref}\n            required={required}\n            value={primitiveValue}\n          >\n            {options.map((option, index) => {\n              const isSelected = selected === option.value;\n              const rowDisabled = disabled || Boolean(option.disabled);\n              const optionId = `${generatedId}-option-${index}`;\n              const labelId = `${optionId}-label`;\n              const descriptionId = option.description\n                ? `${optionId}-description`\n                : undefined;\n\n              return (\n                <RadioPrimitive.Root\n                  aria-describedby={descriptionId}\n                  aria-labelledby={labelId}\n                  disabled={rowDisabled}\n                  key={`${option.value}-${index}`}\n                  nativeButton\n                  render={(rootProps) => (\n                    <BaseRadioOptionButton\n                      description={option.description}\n                      descriptionId={descriptionId}\n                      index={index}\n                      invalid={invalid}\n                      isSelected={isSelected}\n                      label={option.label}\n                      labelId={labelId}\n                      orientation={orientation}\n                      prefersReducedMotion={prefersReducedMotion}\n                      rootProps={rootProps}\n                      rowDisabled={rowDisabled}\n                    />\n                  )}\n                  value={option.value}\n                />\n              );\n            })}\n          </RadioGroupPrimitive>\n        </div>\n      </fieldset>\n    );\n  }\n);\n\nRadioGroup.displayName = \"RadioGroup\";\n\nexport { RadioGroup as radioGroup };\nexport { RadioGroup };\nexport default RadioGroup;\n",
      "type": "registry:ui"
    }
  ],
  "title": "Radio Group (Base UI)",
  "description": "Radio group with the same Iconiq API layered over Base UI primitives, preserving the original shared highlight, spring-loaded ring and dot, and staggered row entrance."
}
