{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"radio-group","type":"registry:ui","title":"Radio Group","description":"A set of options where only one can be selected.","dependencies":["radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/field.json"],"files":[{"path":"registry/dashral/radio-group.tsx","type":"registry:file","target":"components/dashral/radio-group.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"./lib/utils\"\nimport { ChoiceField, FieldLabel, FieldWrapper, splitFieldProps, useFieldControl, type FieldClassNames, type FieldOwnProps } from \"./field\"\n\ntype ControlColor = \"primary\" | \"neutral\" | \"info\" | \"success\" | \"warning\" | \"destructive\" | \"highlight\"\n\n// The dot and border when picked. Every class is written out in full so Tailwind finds it.\nconst radioColors: Record<ControlColor, string> = {\n  primary: \"text-primary data-[state=checked]:border-primary\",\n  neutral: \"text-foreground data-[state=checked]:border-foreground\",\n  info: \"text-info data-[state=checked]:border-info\",\n  success: \"text-success data-[state=checked]:border-success\",\n  warning: \"text-warning data-[state=checked]:border-warning\",\n  destructive: \"text-destructive data-[state=checked]:border-destructive\",\n  highlight: \"text-highlight data-[state=checked]:border-highlight\",\n}\n\n// A picked card: its border and a tint in the color.\nconst cardColors: Record<ControlColor, string> = {\n  primary: \"has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary/5\",\n  neutral: \"has-[[data-state=checked]]:border-foreground has-[[data-state=checked]]:bg-foreground/5\",\n  info: \"has-[[data-state=checked]]:border-info has-[[data-state=checked]]:bg-info/5\",\n  success: \"has-[[data-state=checked]]:border-success has-[[data-state=checked]]:bg-success/5\",\n  warning: \"has-[[data-state=checked]]:border-warning has-[[data-state=checked]]:bg-warning/5\",\n  destructive: \"has-[[data-state=checked]]:border-destructive has-[[data-state=checked]]:bg-destructive/5\",\n  highlight: \"has-[[data-state=checked]]:border-highlight has-[[data-state=checked]]:bg-highlight/5\",\n}\n\ntype RadioOption =\n  | string\n  | { value: string; label?: React.ReactNode; description?: React.ReactNode; icon?: React.ReactNode; disabled?: boolean }\n\ntype RadioGroupProps = Omit<React.ComponentProps<typeof RadioGroupPrimitive.Root>, \"children\"> &\n  FieldOwnProps & {\n    /** The radios: strings, or { value, label, description, icon, disabled }. icon shows on cards. */\n    options: RadioOption[]\n    /** \"cards\" draws each option as a bordered card you click anywhere on. */\n    variant?: \"default\" | \"cards\"\n    /** With cards: false hides the radio dot, and the card's border alone shows what's picked. */\n    indicator?: boolean\n    /** The color of the picked radio (and card). */\n    color?: ControlColor\n    classNames?: FieldClassNames & { group?: string; item?: string; radio?: string }\n  }\n\n/**\n * A set of radios in one tag. label names the whole group; each option can carry its own description and icon, and\n * variant=\"cards\" turns them into cards you click anywhere on.\n */\nfunction RadioGroup(props: RadioGroupProps) {\n  const [field, control] = splitFieldProps(props)\n  return (\n    <FieldWrapper {...field} group>\n      <RadioGroupControl {...control} />\n    </FieldWrapper>\n  )\n}\n\nfunction RadioGroupControl({\n  options,\n  variant = \"default\",\n  indicator = true,\n  color = \"primary\",\n  className,\n  classNames,\n  ...rest\n}: Omit<RadioGroupProps, keyof FieldOwnProps>) {\n  const props = useFieldControl(rest)\n  const groupId = React.useId()\n\n  return (\n    <RadioGroupPrimitive.Root data-slot=\"radio-group\" className={cn(\"grid gap-3\", className, classNames?.group)} {...props}>\n      {options.map((option) => {\n        const { value, label = value, description, icon, disabled } = typeof option === \"string\" ? { value: option } : option\n        const id = `${groupId}-${value}`\n        const radio = (extra?: string, describedBy?: string) => (\n          <Radio\n            id={id}\n            value={value}\n            disabled={disabled}\n            aria-describedby={describedBy}\n            className={cn(radioColors[color], extra, classNames?.radio)}\n          />\n        )\n\n        if (variant === \"cards\") {\n          const descriptionId = description ? `${id}-description` : undefined\n          return (\n            <FieldLabel\n              key={value}\n              htmlFor={id}\n              data-slot=\"radio-card\"\n              className={cn(\n                \"flex items-start gap-3 rounded-lg border p-4 font-normal transition-colors hover:bg-accent/50\",\n                \"has-focus-visible:ring-2 has-focus-visible:ring-ring/50 has-disabled:cursor-not-allowed has-disabled:opacity-50\",\n                cardColors[color],\n                classNames?.item\n              )}\n            >\n              {icon && <span className=\"flex shrink-0 text-muted-foreground [&_svg]:size-5\">{icon}</span>}\n              <span className=\"grid min-w-0 flex-1 gap-1\">\n                <span className=\"leading-4 font-medium\">{label}</span>\n                {description && (\n                  <span id={descriptionId} className=\"text-xs text-muted-foreground\">\n                    {description}\n                  </span>\n                )}\n              </span>\n              {/* Hidden, the radio still takes focus and the arrow keys. */}\n              {radio(indicator ? undefined : \"sr-only\", descriptionId)}\n            </FieldLabel>\n          )\n        }\n\n        return (\n          <ChoiceField key={value} id={id} label={label} description={description} disabled={disabled} leading=\"leading-4\" classNames={{ root: classNames?.item }}>\n            {(choice) => radio(undefined, choice[\"aria-describedby\"])}\n          </ChoiceField>\n        )\n      })}\n    </RadioGroupPrimitive.Root>\n  )\n}\n\nfunction Radio({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n  return (\n    <RadioGroupPrimitive.Item\n      data-slot=\"radio-group-item\"\n      className={cn(\n        \"aspect-square size-4 shrink-0 rounded-full border border-input shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 in-[[aria-invalid=true]]:border-destructive\",\n        className\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator\n        data-slot=\"radio-group-indicator\"\n        // The dot springs in when picked.\n        className=\"relative flex animate-in items-center justify-center zoom-in-0 duration-(--duration-standard) ease-spring motion-reduce:animate-none\"\n      >\n        <span className=\"size-2 rounded-full bg-current\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  )\n}\n\nexport { RadioGroup, type RadioGroupProps, type RadioOption }\n"}],"cssVars":{"theme":{"color-info":"var(--info)","color-info-foreground":"var(--info-foreground)","color-info-text":"var(--info-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","color-warning":"var(--warning)","color-warning-foreground":"var(--warning-foreground)","color-warning-text":"var(--warning-text)","color-highlight":"var(--highlight)","color-highlight-foreground":"var(--highlight-foreground)","color-highlight-text":"var(--highlight-text)","ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"info":"oklch(0.5 0.134 242.749)","info-foreground":"oklch(0.985 0 0)","info-text":"oklch(0.443 0.11 240.79)","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","warning":"#f59200","warning-foreground":"oklch(0.3 0.07 60)","warning-text":"#964e00","highlight":"#b8008c","highlight-foreground":"oklch(0.985 0 0)","highlight-text":"#a3007b","duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{"info":"oklch(0.685 0.169 237.323)","info-foreground":"oklch(0.2 0 0)","info-text":"oklch(0.746 0.16 232.661)","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)","warning":"#f59200","warning-foreground":"oklch(0.25 0.06 60)","warning-text":"#ffba42","highlight":"#ff2ecf","highlight-foreground":"oklch(0.2 0 0)","highlight-text":"#ff75df"}}}