radio-group-stacked.tsx108 lines · main
1'use client'
2
3import { Circle } from 'lucide-react'
4import { RadioGroup as RadioGroupPrimitive } from 'radix-ui'
5import * as React from 'react'
6
7import { Label } from '../components/shadcn/ui/label'
8import { cn } from '../lib/utils/cn'
9
10const RadioGroupStacked = React.forwardRef<
11 React.ElementRef<typeof RadioGroupPrimitive.Root>,
12 React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
13>(({ className, ...props }, ref) => {
14 return (
15 <RadioGroupPrimitive.Root
16 className={cn('flex flex-col -space-y-px w-full', className)}
17 {...props}
18 ref={ref}
19 />
20 )
21})
22
23RadioGroupStacked.displayName = 'RadioGroupStacked'
24
25interface RadioGroupStackedItemProps {
26 image?: React.ReactNode
27 label: React.ReactNode
28 showIndicator?: boolean
29 description?: React.ReactNode
30}
31
32const RadioGroupStackedItem = React.forwardRef<
33 React.ElementRef<typeof RadioGroupPrimitive.Item>,
34 RadioGroupStackedItemProps & React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
35>(({ image, label, showIndicator = true, ...props }, ref) => {
36 return (
37 <RadioGroupPrimitive.Item
38 ref={ref}
39 {...props}
40 className={cn(
41 // Base layout and sizing
42 'flex flex-col gap-2 w-full',
43 // Base styles
44 'bg-overlay/50 border shadow-xs',
45 'first-of-type:rounded-t-lg last-of-type:rounded-b-lg',
46 // Disabled state
47 'disabled:opacity-50 disabled:cursor-not-allowed',
48 // Enabled/hover states
49 'enabled:cursor-pointer enabled:hover:bg-surface-300 enabled:hover:border-foreground-muted',
50 // Z-index for interactions
51 'hover:z-1 focus-visible:z-1 data-[state=checked]:z-1',
52 // Checked state
53 'data-[state=checked]:ring-1 data-[state=checked]:ring-border',
54 'data-[state=checked]:bg-surface-300 data-[state=checked]:border-foreground-muted',
55 // Transitions and group
56 'transition group',
57 props.className
58 )}
59 >
60 <div className="flex gap-3 w-full px-[21px] py-3">
61 {showIndicator && (
62 <div
63 className={cn(
64 // Base styles
65 'aspect-square h-4 w-4 min-w-4 min-h-4 rounded-full border relative',
66 'flex items-center justify-center',
67 'ring-offset-background transition',
68 // States
69 'group-data-[state=checked]:border-foreground-muted',
70 'group-focus:border-foreground-muted group-focus:outline-hidden',
71 'group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2',
72 'group-hover:border-foreground-muted'
73 )}
74 >
75 <RadioGroupPrimitive.Indicator className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
76 <Circle size={10} strokeWidth={0} className="fill-current text-current" />
77 </RadioGroupPrimitive.Indicator>
78 </div>
79 )}
80 <div className="flex flex-col gap-0.25 items-start">
81 <Label
82 htmlFor={props.value}
83 className={cn(
84 // Base styles
85 'block mt-[-0.15rem] text-sm text-left text-light',
86 // Transitions
87 'transition-colors',
88 // States
89 'enabled:group-hover:text-foreground group-data-[state=checked]:text-foreground'
90 )}
91 >
92 {label}
93 </Label>
94 {props.description && (
95 <p className="text-left text-sm text-foreground-lighter text-balance">
96 {props.description}
97 </p>
98 )}
99 {props.children}
100 </div>
101 </div>
102 </RadioGroupPrimitive.Item>
103 )
104})
105
106RadioGroupStackedItem.displayName = 'RadioGroupStackedItem'
107
108export { RadioGroupStacked, RadioGroupStackedItem }