{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","radix-ui"],"description":"An animated Radio Group component for SmoothUI with selection indicator spring animation.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\nimport type React from \"react\";\nimport { Children, cloneElement, isValidElement, useId } from \"react\";\nimport { SPRING_DEFAULT } from \"@/components/smoothui/lib/animation\";\n\nexport interface RadioGroupProps {\n  /** Radio items to render */\n  children: React.ReactNode;\n  /** Optional CSS class for the group container */\n  className?: string;\n  /** The default value when uncontrolled */\n  defaultValue?: string;\n  /** Whether the entire group is disabled */\n  disabled?: boolean;\n  /** Name attribute for form submission */\n  name?: string;\n  /** Callback when the selected value changes */\n  onValueChange?: (value: string) => void;\n  /** Orientation of the radio group for arrow key navigation */\n  orientation?: \"horizontal\" | \"vertical\";\n  /** Whether a selection is required */\n  required?: boolean;\n  /** The controlled value of the selected radio item */\n  value?: string;\n}\n\nexport interface RadioProps {\n  /** @internal Stagger index passed by RadioGroup */\n  _staggerIndex?: number;\n  /** Children (label content) to render next to the radio */\n  children?: React.ReactNode;\n  /** Optional CSS class */\n  className?: string;\n  /** Whether this radio is disabled */\n  disabled?: boolean;\n  /** ID for label association */\n  id?: string;\n  /** The value of this radio option */\n  value: string;\n}\n\n/** Spring for the selection dot — playful bounce per CLAUDE.md (0.2-0.3 for playful interactions) */\nconst SPRING_DOT = {\n  bounce: 0.2,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\n/** Stagger delay per radio item on initial render */\nconst STAGGER_DELAY = 0.04;\n\nexport function RadioGroup({\n  value,\n  defaultValue,\n  onValueChange,\n  disabled = false,\n  className,\n  children,\n  name,\n  required,\n  orientation = \"vertical\",\n}: RadioGroupProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  // Inject stagger index into Radio children for entrance stagger\n  let radioIndex = 0;\n  const enhancedChildren = Children.map(children, (child) => {\n    if (\n      isValidElement(child) &&\n      (child.type === Radio ||\n        (child.type as { displayName?: string })?.displayName === \"Radio\")\n    ) {\n      const currentIndex = radioIndex;\n      radioIndex += 1;\n      return cloneElement(child as React.ReactElement<RadioProps>, {\n        _staggerIndex: currentIndex,\n      });\n    }\n    return child;\n  });\n\n  return (\n    <RadioGroupPrimitive.Root\n      className={cn(\n        orientation === \"vertical\" ? \"grid gap-3\" : \"flex items-center gap-4\",\n        className\n      )}\n      data-slot=\"radio-group\"\n      defaultValue={defaultValue}\n      disabled={disabled}\n      name={name}\n      onValueChange={onValueChange}\n      orientation={orientation}\n      required={required}\n      value={value}\n    >\n      {/* biome-ignore lint/suspicious/noLeakedRender: both branches are ReactNode, which is exactly what a wrapper should render */}\n      {shouldReduceMotion ? children : enhancedChildren}\n    </RadioGroupPrimitive.Root>\n  );\n}\n\nexport function Radio({\n  value,\n  disabled = false,\n  className,\n  id,\n  children,\n  _staggerIndex,\n}: RadioProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const staggerDelay =\n    _staggerIndex === undefined ? 0 : _staggerIndex * STAGGER_DELAY;\n  // Radix renders the item as a <button role=\"radio\"> with no inner text, so\n  // its only accessible name is the sibling <label>. Without a generated\n  // fallback the label's htmlFor pointed nowhere whenever the consumer omitted\n  // `id`, leaving the control nameless.\n  const generatedId = useId();\n  const radioId = id ?? generatedId;\n\n  const wrapper = (content: React.ReactNode) => {\n    if (shouldReduceMotion || _staggerIndex === undefined) {\n      return <div className=\"flex items-center gap-2\">{content}</div>;\n    }\n    return (\n      <motion.div\n        animate={{ opacity: 1, transform: \"translateY(0px)\" }}\n        className=\"flex items-center gap-2\"\n        initial={{ opacity: 0, transform: \"translateY(4px)\" }}\n        transition={{\n          ...SPRING_DEFAULT,\n          delay: staggerDelay,\n        }}\n      >\n        {content}\n      </motion.div>\n    );\n  };\n\n  return wrapper(\n    <>\n      <motion.div\n        className=\"relative\"\n        transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n        whileHover={shouldReduceMotion ? {} : { transform: \"scale(1.1)\" }}\n      >\n        <RadioGroupPrimitive.Item\n          className={cn(\n            \"group/radio aspect-square size-4 shrink-0 rounded-full border border-input bg-background shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-brand dark:bg-input/30 dark:aria-invalid:ring-destructive/40\",\n            className\n          )}\n          data-slot=\"radio-group-item\"\n          disabled={disabled}\n          id={radioId}\n          value={value}\n        >\n          <RadioGroupPrimitive.Indicator\n            className=\"flex items-center justify-center\"\n            data-slot=\"radio-group-indicator\"\n          >\n            <motion.span\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 1, transform: \"scale(1)\" }\n              }\n              className=\"block size-2 rounded-full bg-brand\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : {\n                      opacity: 0,\n                      transform: \"scale(0)\",\n                      transition: { ...SPRING_DEFAULT, bounce: 0 },\n                    }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, transform: \"scale(0)\" }\n              }\n              transition={shouldReduceMotion ? { duration: 0 } : SPRING_DOT}\n            />\n          </RadioGroupPrimitive.Indicator>\n        </RadioGroupPrimitive.Item>\n      </motion.div>\n      {children ? (\n        <label\n          className={cn(\n            \"font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n            disabled && \"cursor-not-allowed opacity-70\"\n          )}\n          htmlFor={radioId}\n        >\n          {children}\n        </label>\n      ) : null}\n    </>\n  );\n}\n\nRadio.displayName = \"Radio\";\n\nexport default RadioGroup;\n","path":"index.tsx","target":"components/smoothui/radio-group/index.tsx","type":"registry:ui"}],"name":"radio-group","registryDependencies":["https://smoothui.dev/r/lib.json","https://smoothui.dev/r/tokens.json"],"title":"Radio Group","type":"registry:ui"}