{
  "name": "radio-group",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "radio-group.tsx",
      "content": "\"use client\";\r\n\r\nimport { css, themeVars as theme } from \"@yugnex/core\";\r\nimport { useControllableState } from \"@yugnex/core/client\";\r\nimport {\r\n  createContext,\r\n  useCallback,\r\n  useContext,\r\n  useId,\r\n  useRef,\r\n  useState,\r\n  type HTMLAttributes,\r\n  type KeyboardEvent,\r\n  type ReactNode,\r\n} from \"react\";\r\n\r\ninterface RadioGroupContextValue {\r\n  name: string;\r\n  value: string | undefined;\r\n  setValue: (value: string) => void;\r\n  register: (value: string, el: HTMLButtonElement | null) => void;\r\n  onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;\r\n  /** True for the one item that should hold the group's single tab stop. */\r\n  isTabbable: (value: string) => boolean;\r\n  disabled?: boolean;\r\n}\r\n\r\nconst RadioGroupContext = createContext<RadioGroupContextValue | null>(null);\r\n\r\nfunction useRadioGroupContext(component: string): RadioGroupContextValue {\r\n  const ctx = useContext(RadioGroupContext);\r\n  if (!ctx) throw new Error(`<${component}> must be used within <RadioGroup>`);\r\n  return ctx;\r\n}\r\n\r\nconst groupClass = css({\r\n  display: \"flex\",\r\n  flexDirection: \"column\",\r\n  gap: theme.space[2],\r\n});\r\n\r\nconst itemRowClass = css({\r\n  display: \"flex\",\r\n  alignItems: \"flex-start\",\r\n  gap: theme.space[2.5],\r\n});\r\n\r\nconst radioClass = css({\r\n  position: \"relative\",\r\n  flexShrink: 0,\r\n  width: \"1.125rem\",\r\n  height: \"1.125rem\",\r\n  marginTop: \"1px\",\r\n  borderRadius: \"9999px\",\r\n  border: `1px solid ${theme.color.input}`,\r\n  backgroundColor: theme.color.background,\r\n  cursor: \"pointer\",\r\n  padding: 0,\r\n  transitionProperty: \"border-color, box-shadow\",\r\n  transitionDuration: theme.duration.fast,\r\n  '&[data-state=\"checked\"]': { borderColor: theme.color.primary },\r\n  \"&:focus-visible\": { outline: `2px solid ${theme.color.ring}`, outlineOffset: \"2px\" },\r\n  \"&:disabled\": { opacity: 0.5, cursor: \"not-allowed\" },\r\n});\r\n\r\nconst dotClass = css({\r\n  position: \"absolute\",\r\n  top: \"50%\",\r\n  left: \"50%\",\r\n  width: \"0.5rem\",\r\n  height: \"0.5rem\",\r\n  borderRadius: \"9999px\",\r\n  backgroundColor: theme.color.primary,\r\n  transform: \"translate(-50%, -50%) scale(0)\",\r\n  transitionProperty: \"transform\",\r\n  transitionDuration: theme.duration.fast,\r\n  transitionTimingFunction: theme.easing.decelerate,\r\n  '[data-state=\"checked\"] &': { transform: \"translate(-50%, -50%) scale(1)\" },\r\n});\r\n\r\nconst labelColumnClass = css({ display: \"flex\", flexDirection: \"column\", gap: \"2px\", minWidth: 0 });\r\n\r\nconst labelClass = css({\r\n  fontFamily: theme.fontFamily.sans,\r\n  fontSize: theme.fontSize.sm,\r\n  color: theme.color.foreground,\r\n  cursor: \"pointer\",\r\n});\r\n\r\nconst descriptionClass = css({\r\n  fontFamily: theme.fontFamily.sans,\r\n  fontSize: theme.fontSize.xs,\r\n  color: theme.color.mutedForeground,\r\n});\r\n\r\nexport interface RadioGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onChange\" | \"defaultValue\"> {\r\n  value?: string;\r\n  defaultValue?: string;\r\n  onValueChange?: (value: string) => void;\r\n  /** Shared form field name for the group. Auto-generated when omitted. */\r\n  name?: string;\r\n  disabled?: boolean;\r\n  children: ReactNode;\r\n  label?: string;\r\n}\r\n\r\n/**\r\n * A single-select group. Follows the WAI-ARIA radio pattern: only the checked\r\n * radio is tabbable, and arrow keys move selection between options.\r\n */\r\nexport function RadioGroup({\r\n  value,\r\n  defaultValue,\r\n  onValueChange,\r\n  name,\r\n  disabled,\r\n  label,\r\n  children,\r\n  className,\r\n  ...props\r\n}: RadioGroupProps) {\r\n  const [selected, setSelected] = useControllableState<string | undefined>({\r\n    value,\r\n    defaultValue,\r\n    onChange: (next) => next != null && onValueChange?.(next),\r\n  });\r\n\r\n  const generatedName = useId();\r\n  const itemsRef = useRef<Map<string, HTMLButtonElement>>(new Map());\r\n\r\n  // With nothing selected the group still needs exactly one tab stop, which by\r\n  // convention is the first item. Items can't know their own position, so the\r\n  // first one to attach its ref (mount order, i.e. DOM order) claims it.\r\n  const [firstValue, setFirstValue] = useState<string | null>(null);\r\n\r\n  const register = useCallback((itemValue: string, el: HTMLButtonElement | null) => {\r\n    if (el) {\r\n      itemsRef.current.set(itemValue, el);\r\n      setFirstValue((prev) => prev ?? itemValue);\r\n    } else {\r\n      itemsRef.current.delete(itemValue);\r\n      setFirstValue((prev) => (prev === itemValue ? null : prev));\r\n    }\r\n  }, []);\r\n\r\n  const isTabbable = useCallback(\r\n    (itemValue: string) => (selected != null ? selected === itemValue : firstValue === itemValue),\r\n    [selected, firstValue],\r\n  );\r\n\r\n  const onKeyDown = useCallback(\r\n    (event: KeyboardEvent<HTMLButtonElement>) => {\r\n      const entries = Array.from(itemsRef.current.entries()).filter(\r\n        ([, el]) => el.isConnected && !el.disabled,\r\n      );\r\n      if (entries.length === 0) return;\r\n      const index = entries.findIndex(([, el]) => el === event.currentTarget);\r\n      if (index === -1) return;\r\n\r\n      let next: number | null = null;\r\n      if (event.key === \"ArrowDown\" || event.key === \"ArrowRight\") next = (index + 1) % entries.length;\r\n      else if (event.key === \"ArrowUp\" || event.key === \"ArrowLeft\")\r\n        next = (index - 1 + entries.length) % entries.length;\r\n      else if (event.key === \"Home\") next = 0;\r\n      else if (event.key === \"End\") next = entries.length - 1;\r\n\r\n      if (next !== null) {\r\n        event.preventDefault();\r\n        const entry = entries[next];\r\n        if (!entry) return;\r\n        const [nextValue, nextEl] = entry;\r\n        // The radio pattern moves selection with focus, not just focus alone.\r\n        setSelected(nextValue);\r\n        nextEl.focus();\r\n      }\r\n    },\r\n    [setSelected],\r\n  );\r\n\r\n  return (\r\n    <RadioGroupContext.Provider\r\n      value={{\r\n        name: name ?? generatedName,\r\n        value: selected,\r\n        setValue: setSelected,\r\n        register,\r\n        onKeyDown,\r\n        isTabbable,\r\n        disabled,\r\n      }}\r\n    >\r\n      <div\r\n        role=\"radiogroup\"\r\n        aria-label={label}\r\n        className={className ? `${groupClass} ${className}` : groupClass}\r\n        {...props}\r\n      >\r\n        {children}\r\n      </div>\r\n    </RadioGroupContext.Provider>\r\n  );\r\n}\r\n\r\nexport interface RadioGroupItemProps {\r\n  value: string;\r\n  children?: ReactNode;\r\n  description?: ReactNode;\r\n  disabled?: boolean;\r\n  className?: string;\r\n}\r\n\r\nexport function RadioGroupItem({ value, children, description, disabled, className }: RadioGroupItemProps) {\r\n  const ctx = useRadioGroupContext(\"RadioGroupItem\");\r\n  const id = useId();\r\n  const checked = ctx.value === value;\r\n  const isDisabled = disabled || ctx.disabled;\r\n\r\n  return (\r\n    <div className={className ? `${itemRowClass} ${className}` : itemRowClass}>\r\n      <button\r\n        type=\"button\"\r\n        role=\"radio\"\r\n        id={id}\r\n        ref={(el) => ctx.register(value, el)}\r\n        aria-checked={checked}\r\n        data-state={checked ? \"checked\" : \"unchecked\"}\r\n        // Roving tabindex: the group is one tab stop, arrows move within it.\r\n        tabIndex={ctx.isTabbable(value) ? 0 : -1}\r\n        disabled={isDisabled}\r\n        className={radioClass}\r\n        onClick={() => ctx.setValue(value)}\r\n        onKeyDown={ctx.onKeyDown}\r\n      >\r\n        <span className={dotClass} />\r\n      </button>\r\n      {children || description ? (\r\n        <div className={labelColumnClass}>\r\n          {children ? (\r\n            <label htmlFor={id} className={labelClass}>\r\n              {children}\r\n            </label>\r\n          ) : null}\r\n          {description ? <span className={descriptionClass}>{description}</span> : null}\r\n        </div>\r\n      ) : null}\r\n    </div>\r\n  );\r\n}\r\n",
      "type": "registry:component"
    }
  ]
}