"use client" /** * Radio group — Radix `RadioGroup` + `RadioGroupItem` with Exxat styling. * * Aligned with Shadcn Studio radio-group guidance (see shadcnstudio.com/docs/components/radio-group): * • Visual variants: default, outline, secondary, success, destructive, warning, muted * • Sizes: sm, default, lg (outer ring + inner dot scale together) * • Motion: none | pop | glow | pop-glow — `motion-safe` / `motion-reduce` (WCAG 2.3.3) * • Optional group defaults: `` so items inherit * • Per-item overrides: `` * • Forms: `aria-invalid`, `disabled`, hit slop (`after:`); pair with `RadioGroupLabel` + `htmlFor` / `id` */ import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { RadioGroup as RadioGroupPrimitive } from "radix-ui" import { cn } from "../../lib/utils" import { Label } from "./label" // ── Item chrome (variants / sizes / motion) ───────────────────────────────── const radioGroupItemVariants = cva( [ "group/radio-group-item peer relative flex shrink-0 aspect-square rounded-full border border-input", "outline-none transition-[color,box-shadow,transform,background-color,border-color] duration-150", "motion-reduce:transition-none", "group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2", "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50", "disabled:cursor-not-allowed disabled:opacity-50", "aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:data-[state=checked]:border-primary", "dark:bg-input/15 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", ].join(" "), { variants: { variant: { default: [ "data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary", ].join(" "), outline: [ "bg-background", "data-[state=checked]:border-primary data-[state=checked]:bg-background data-[state=checked]:text-primary data-[state=checked]:ring-2 data-[state=checked]:ring-primary/25", ].join(" "), secondary: [ "data-[state=checked]:border-secondary data-[state=checked]:bg-secondary data-[state=checked]:text-secondary-foreground", ].join(" "), success: [ "data-[state=checked]:border-chart-2 data-[state=checked]:bg-chart-2 data-[state=checked]:text-primary-foreground", ].join(" "), destructive: [ "data-[state=checked]:border-destructive data-[state=checked]:bg-destructive data-[state=checked]:text-destructive-foreground", ].join(" "), warning: [ "data-[state=checked]:border-amber-500 data-[state=checked]:bg-amber-500 data-[state=checked]:text-amber-950", ].join(" "), muted: [ "data-[state=checked]:border-muted-foreground/50 data-[state=checked]:bg-muted data-[state=checked]:text-foreground", ].join(" "), }, size: { sm: "size-3.5 min-h-3.5 min-w-3.5 max-h-3.5 max-w-3.5", default: "size-4 min-h-4 min-w-4 max-h-4 max-w-4", lg: "size-5 min-h-5 min-w-5 max-h-5 max-w-5", }, motion: { none: "", pop: [ "motion-safe:active:scale-95", "data-[state=checked]:motion-safe:scale-[1.04]", ].join(" "), glow: "data-[state=checked]:shadow-[0_0_0_3px] data-[state=checked]:shadow-primary/35", "pop-glow": [ "motion-safe:active:scale-95", "data-[state=checked]:motion-safe:scale-[1.04]", "data-[state=checked]:shadow-[0_0_0_3px] data-[state=checked]:shadow-primary/35", ].join(" "), }, }, defaultVariants: { variant: "default", size: "default", motion: "none", }, } ) const radioIndicatorDotVariants = cva( "pointer-events-none absolute top-1/2 start-1/2 -translate-x-1/2 rtl:translate-x-1/2 -translate-y-1/2 rounded-full", { variants: { variant: { default: "bg-primary-foreground", outline: "bg-primary", secondary: "bg-secondary-foreground", success: "bg-primary-foreground", destructive: "bg-destructive-foreground", warning: "bg-amber-950", muted: "bg-foreground", }, size: { sm: "size-1.5", default: "size-2", lg: "size-2.5", }, }, defaultVariants: { variant: "default", size: "default", }, } ) const radioIndicatorWrapperVariants = cva("relative flex size-full items-center justify-center", { variants: { motion: { none: "", pop: "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:duration-150", glow: "", "pop-glow": "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:duration-150", }, }, defaultVariants: { motion: "none" }, }) type ItemChrome = { itemVariant?: VariantProps["variant"] itemSize?: VariantProps["size"] itemMotion?: VariantProps["motion"] } const RadioGroupItemChromeContext = React.createContext({}) // ── RadioGroup root ───────────────────────────────────────────────────────── export type RadioGroupProps = React.ComponentPropsWithoutRef & ItemChrome function RadioGroup({ className, itemVariant, itemSize, itemMotion, ...props }: RadioGroupProps) { const ctx = React.useMemo( () => ({ itemVariant, itemSize, itemMotion }), [itemVariant, itemSize, itemMotion], ) return ( ) } // ── RadioGroupItem ────────────────────────────────────────────────────────── export type RadioGroupItemProps = React.ComponentPropsWithoutRef & VariantProps const RadioGroupItem = React.forwardRef< React.ElementRef, RadioGroupItemProps >(function RadioGroupItem( { className, variant: variantProp, size: sizeProp, motion: motionProp, ...props }, ref, ) { const ctx = React.useContext(RadioGroupItemChromeContext) const variant = variantProp ?? ctx.itemVariant const size = sizeProp ?? ctx.itemSize const motion = motionProp ?? ctx.itemMotion ?? "none" const vResolved = variant ?? "default" const sResolved = size ?? "default" return ( ) }) // ── Label helper (touch-friendly row with peer-disabled) ──────────────────── export type RadioGroupLabelProps = React.ComponentPropsWithRef function RadioGroupLabel({ className, ...props }: RadioGroupLabelProps) { return (