{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"radio-group","title":"Radio Group","description":"Base UI-backed single-selection form group.","dependencies":["@base-ui/react@1.7.0","class-variance-authority@0.7.1"],"registryDependencies":["@siteplane/base"],"files":[{"path":"packages/ui/src/components/radio-group/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 { cva } from \"class-variance-authority\";\nimport type React from \"react\";\nimport { createContext, useContext } from \"react\";\n\nimport {\n  type SiteplaneControlSize,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport {\n  ChoiceMenuItemCheck,\n  choiceMenuItemCheckGridClassName,\n  choiceMenuItemClassName,\n  choiceMenuItemEndGridClassName,\n  choiceMenuItemEndLabelClassName,\n  choiceMenuItemLabelClassName,\n  menuRowLabel,\n} from \"../../system/styles/choice-menu-item\";\nimport { cn, type WithStringClassName } from \"../../system/utils/cn\";\nimport { baseClass, slotClass } from \"../../system/utils/slot-class\";\nimport type { KitRadioGroupProps, KitRadioProps } from \"./radio-group-internal\";\n\nimport \"./radio-group.css\";\n\n// The look and the size come from radio-group.css through the attributes the\n// Radio renders (data-size), so the helper takes no size.\nexport const radioVariants = cva(\n  \"sp-radio-motion relative inline-flex shrink-0 cursor-pointer items-center justify-center before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] data-disabled:cursor-not-allowed\",\n);\n\nconst radioIndicatorClassName = \"absolute flex items-center justify-center\";\n\nconst RadioSizeContext = createContext<SiteplaneControlSize | null>(null);\n\nexport interface RadioGroupProps extends Omit<\n  WithStringClassName<RadioGroupPrimitive.Props>,\n  \"size\"\n> {\n  size?: SiteplaneControlSize;\n}\n\nexport function RadioGroup(groupProps: RadioGroupProps): React.ReactElement {\n  // Menu and drawer rows pass the internal menu-item profile through\n  // KitRadioGroup (radio-group-internal.ts), the same function.\n  const {\n    className,\n    profile = \"control\",\n    size,\n    ...props\n  } = groupProps as KitRadioGroupProps;\n  const resolvedSize = useResolvedControlSize(size);\n\n  return (\n    <RadioSizeContext.Provider value={resolvedSize}>\n      <RadioGroupPrimitive\n        className={cn(\n          slotClass(\"radio-group\", props),\n          baseClass(\"radio-group\", props),\n          \"flex flex-col\",\n          // The 8 px default is a utility, so a gap in className replaces it.\n          profile === \"control\" && \"gap-2\",\n          className,\n        )}\n        data-foundation=\"radio-group\"\n        data-profile={profile}\n        data-size={resolvedSize}\n        data-slot=\"radio-group\"\n        {...props}\n      />\n    </RadioSizeContext.Provider>\n  );\n}\n\nexport interface RadioProps extends Omit<\n  WithStringClassName<RadioPrimitive.Root.Props>,\n  \"size\"\n> {\n  size?: SiteplaneControlSize;\n}\n\nexport function Radio(radioProps: RadioProps): React.ReactElement {\n  // Menu and drawer rows pass the internal menu-item profile through KitRadio\n  // (radio-group-internal.ts), the same function.\n  const {\n    children,\n    className,\n    indicatorPosition = \"start\",\n    profile = \"control\",\n    size,\n    ...props\n  } = radioProps as KitRadioProps;\n  const groupSize = useContext(RadioSizeContext);\n  const resolvedSize = useResolvedControlSize(size ?? groupSize);\n  // An identity row of a menu, such as a workspace in a switcher, leads with\n  // its logo or avatar and ends with its check, as MenuRadioItem's end rows.\n  const end = profile === \"menu-item\" && indicatorPosition === \"end\";\n\n  return (\n    <RadioPrimitive.Root\n      className={cn(\n        slotClass(\"radio\", props),\n        baseClass(\"radio\", props),\n        profile === \"menu-item\"\n          ? cn(\n              choiceMenuItemClassName,\n              end\n                ? choiceMenuItemEndGridClassName\n                : choiceMenuItemCheckGridClassName,\n            )\n          : radioVariants(),\n        className,\n      )}\n      data-foundation=\"radio\"\n      data-indicator-position={end ? \"end\" : undefined}\n      data-profile={profile}\n      data-size={resolvedSize}\n      data-slot=\"radio\"\n      {...props}\n    >\n      {end ? (\n        <>\n          <span className={choiceMenuItemEndLabelClassName}>\n            {menuRowLabel(children)}\n          </span>\n          <RadioPrimitive.Indicator\n            className=\"sp-radio-indicator col-start-2\"\n            data-slot=\"radio-indicator\"\n            keepMounted\n          >\n            <ChoiceMenuItemCheck />\n          </RadioPrimitive.Indicator>\n        </>\n      ) : profile === \"menu-item\" ? (\n        <>\n          <RadioPrimitive.Indicator\n            className=\"sp-radio-indicator col-start-1\"\n            data-slot=\"radio-indicator\"\n            keepMounted\n          >\n            <ChoiceMenuItemCheck />\n          </RadioPrimitive.Indicator>\n          <span className={choiceMenuItemLabelClassName}>{children}</span>\n        </>\n      ) : (\n        <RadioPrimitive.Indicator\n          className={cn(\"sp-radio-indicator\", radioIndicatorClassName)}\n          data-slot=\"radio-indicator\"\n          keepMounted\n        />\n      )}\n    </RadioPrimitive.Root>\n  );\n}\n\nexport { RadioGroupPrimitive, RadioPrimitive };\n","type":"registry:ui","target":"~/src/siteplane/components/radio-group/radio-group.tsx"},{"path":"packages/ui/src/components/radio-group/radio-group-internal.ts","content":"import type * as React from \"react\";\n\nimport {\n  Radio,\n  RadioGroup,\n  type RadioGroupProps,\n  type RadioProps,\n} from \"./radio-group\";\n\n/*\n * Kit-internal RadioGroup and Radio. Not public API: the radio-group index\n * does not export this module, and its names and profiles may change in any\n * release. Menu and Drawer radio rows render them with the menu-item profile.\n */\nexport type RadioProfile = \"control\" | \"menu-item\";\n\nexport type KitRadioGroupProps = RadioGroupProps & { profile?: RadioProfile };\n\nexport type KitRadioProps = RadioProps & {\n  profile?: RadioProfile;\n  /**\n   * In the menu-item profile: where the row shows its check, in the leading\n   * slot (`start`) or at the end of an identity row (`end`), as MenuRadioItem\n   * does.\n   */\n  indicatorPosition?: \"start\" | \"end\" | undefined;\n};\n\n/** RadioGroup with the kit's composition profile; the same component. */\nexport const KitRadioGroup = RadioGroup as (\n  props: KitRadioGroupProps,\n) => React.ReactElement;\n\n/** Radio with the kit's composition profile; the same component. */\nexport const KitRadio = Radio as (props: KitRadioProps) => React.ReactElement;\n","type":"registry:ui","target":"~/src/siteplane/components/radio-group/radio-group-internal.ts"},{"path":"packages/ui/src/components/radio-group/radio-group.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer components {\n  /* Rulebook I5: a radio before text centres on the first text line, also\n   * when the text wraps, as the Checkbox does (checkbox.css). A radio taller\n   * than the line starts at the line's top. */\n  :where(label, .sp-label, .sp-field-label)\n    > .sp-base-radio[data-profile=\"control\"]:first-child,\n  :where(label, .sp-label, .sp-field-label)\n    > :where(span, div)\n    > .sp-base-radio[data-profile=\"control\"]:first-child,\n  .sp-base-radio[data-profile=\"control\"]:first-child:has(\n      ~ :is(.sp-label, .sp-field-label),\n      ~ * :is(.sp-label, .sp-field-label)\n    ) {\n    align-self: flex-start;\n    margin-block: max(0px, calc((1lh - var(--sp-rw-choice-size)) / 2));\n  }\n\n  /* Beside its label, the radio does not inherit the label's line but its\n   * container's. It takes the 21 px line of a label next to a choice control\n   * (S13), so the 1lh above measures the label's first line. */\n  .sp-base-radio[data-profile=\"control\"]:first-child:has(\n      ~ :is(.sp-label, .sp-field-label),\n      ~ * :is(.sp-label, .sp-field-label)\n    ) {\n    line-height: var(--sp-rw-line-21);\n  }\n\n  /* The indicator pops and fades with the Checkbox's motion: the same slow\n   * duration, standard curve and pop scale, and the same linger on the way\n   * out (--motion-duration-linger). Every duration and curve is a\n   * motion token (rulebook M1). */\n  .sp-radio-motion .sp-radio-indicator {\n    background-color: transparent;\n    opacity: 0;\n    transform: scale(var(--motion-check-pop-scale));\n    transform-origin: center;\n    transition-duration: var(--motion-duration-slow);\n    transition-property: background-color, opacity, transform;\n    transition-timing-function: var(--motion-ease-standard);\n  }\n\n  .sp-radio-motion .sp-radio-indicator[data-checked] {\n    background-color: var(--sp-semantic-color-primary);\n    opacity: 1;\n    transform: scale(1);\n  }\n\n  .sp-radio-motion\n    .sp-radio-indicator:is([data-starting-style], [data-ending-style]) {\n    background-color: transparent;\n    opacity: 0;\n    transform: scale(var(--motion-check-pop-scale));\n  }\n\n  .sp-radio-motion .sp-radio-indicator[data-ending-style] {\n    transition-duration: var(--motion-duration-linger);\n  }\n\n  /* The dot grows after its box has started to pop: half a base duration. */\n  .sp-radio-motion .sp-radio-indicator::before {\n    transform: scale(0);\n    transform-origin: center;\n    transition-duration: var(--motion-duration-slow);\n    transition-delay: calc(var(--motion-duration-base) / 2);\n    transition-property: transform;\n    transition-timing-function: var(--motion-ease-standard);\n  }\n\n  .sp-radio-motion .sp-radio-indicator[data-checked]::before {\n    transform: scale(1);\n  }\n\n  .sp-radio-motion\n    .sp-radio-indicator:is([data-starting-style], [data-ending-style])::before {\n    transform: scale(0);\n  }\n\n  .sp-radio-motion .sp-radio-indicator[data-ending-style]::before {\n    transition-delay: 0ms;\n    transition-duration: var(--motion-duration-linger);\n  }\n\n  html[data-motion-reduce=\"true\"] .sp-radio-motion .sp-radio-indicator,\n  html[data-motion-reduce=\"true\"] .sp-radio-motion .sp-radio-indicator::before,\n  [data-sp-motion=\"reduced\"] .sp-radio-motion .sp-radio-indicator,\n  [data-sp-motion=\"reduced\"] .sp-radio-motion .sp-radio-indicator::before {\n    animation: none;\n    transform: none;\n    transition-delay: 0ms;\n    transition-duration: 1ms;\n  }\n}\n\n/* The reduced-motion override sits in the kit layer, where it keeps its order\n * against design-rework.css. */\n@layer siteplane {\n  @media (prefers-reduced-motion: reduce) {\n    .sp-radio-motion .sp-radio-indicator,\n    .sp-radio-motion .sp-radio-indicator::before {\n      animation: none;\n      transform: none;\n      transition-delay: 0ms;\n      transition-duration: 1ms;\n    }\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/radio-group/radio-group.css"},{"path":"packages/ui/src/components/radio-group/index.ts","content":"export * from \"./radio-group\";\n","type":"registry:ui","target":"~/src/siteplane/components/radio-group/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/radio-group.md","categories":["Forms & selection"],"meta":{"avoidWhen":["Avoid for several independent choices; use Checkbox Group.","Avoid for a long list of options; use Select."],"description":"Base UI-backed single-selection form group.","knownLimitations":[],"providerNotes":[],"releaseStatus":"stable","searchTerms":["radio-group","radio group","radio","group"],"useWhen":["Use to choose exactly one option from a short, visible set."]}}