11"use client" ;
22
3+ import { motion } from "framer-motion" ;
34import * as TabsPrimitive from "@radix-ui/react-tabs" ;
45import * as React from "react" ;
56import { cn } from "~/utils/cn" ;
7+ import { type Variants } from "./Tabs" ;
8+
9+ type ClientTabsContextValue = {
10+ value ?: string ;
11+ } ;
12+
13+ const ClientTabsContext = React . createContext < ClientTabsContextValue | undefined > ( undefined ) ;
14+
15+ function useClientTabsContext ( ) {
16+ return React . useContext ( ClientTabsContext ) ;
17+ }
618
719const ClientTabs = React . forwardRef <
820 React . ElementRef < typeof TabsPrimitive . Root > ,
921 React . ComponentPropsWithoutRef < typeof TabsPrimitive . Root >
10- > ( ( props , ref ) => < TabsPrimitive . Root ref = { ref } { ...props } /> ) ;
22+ > ( ( { onValueChange, value : valueProp , defaultValue, ...props } , ref ) => {
23+ const [ value , setValue ] = React . useState < string | undefined > ( valueProp ?? defaultValue ) ;
24+
25+ React . useEffect ( ( ) => {
26+ if ( valueProp !== undefined ) {
27+ setValue ( valueProp ) ;
28+ }
29+ } , [ valueProp ] ) ;
30+
31+ const handleValueChange = React . useCallback (
32+ ( nextValue : string ) => {
33+ if ( valueProp === undefined ) {
34+ setValue ( nextValue ) ;
35+ }
36+ onValueChange ?.( nextValue ) ;
37+ } ,
38+ [ onValueChange , valueProp ]
39+ ) ;
40+
41+ const controlledProps =
42+ valueProp !== undefined
43+ ? { value : valueProp }
44+ : defaultValue !== undefined
45+ ? { defaultValue }
46+ : { } ;
47+
48+ const contextValue = React . useMemo < ClientTabsContextValue > ( ( ) => ( { value } ) , [ value ] ) ;
49+
50+ return (
51+ < ClientTabsContext . Provider value = { contextValue } >
52+ < TabsPrimitive . Root
53+ ref = { ref }
54+ onValueChange = { handleValueChange }
55+ { ...controlledProps }
56+ { ...props }
57+ />
58+ </ ClientTabsContext . Provider >
59+ ) ;
60+ } ) ;
1161ClientTabs . displayName = TabsPrimitive . Root . displayName ;
1262
1363const ClientTabsList = React . forwardRef <
1464 React . ElementRef < typeof TabsPrimitive . List > ,
15- React . ComponentPropsWithoutRef < typeof TabsPrimitive . List >
16- > ( ( { className, ...props } , ref ) => (
17- < TabsPrimitive . List
18- ref = { ref }
19- className = { cn ( "inline-flex items-center justify-center transition duration-100" , className ) }
20- { ...props }
21- />
22- ) ) ;
65+ React . ComponentPropsWithoutRef < typeof TabsPrimitive . List > & {
66+ variant ?: Variants ;
67+ }
68+ > ( ( { className, variant = "pipe-divider" , ...props } , ref ) => {
69+ const variantClassName = ( ( ) => {
70+ switch ( variant ) {
71+ case "segmented" :
72+ return "relative flex h-10 w-full items-center rounded bg-charcoal-700/50 p-1" ;
73+ case "underline" :
74+ return "flex gap-x-6 border-b border-grid-bright" ;
75+ default :
76+ return "inline-flex items-center justify-center transition duration-100" ;
77+ }
78+ } ) ( ) ;
79+
80+ return < TabsPrimitive . List ref = { ref } className = { cn ( variantClassName , className ) } { ...props } /> ;
81+ } ) ;
2382ClientTabsList . displayName = TabsPrimitive . List . displayName ;
2483
2584const ClientTabsTrigger = React . forwardRef <
2685 React . ElementRef < typeof TabsPrimitive . Trigger > ,
27- React . ComponentPropsWithoutRef < typeof TabsPrimitive . Trigger >
28- > ( ( { className, ...props } , ref ) => (
29- < TabsPrimitive . Trigger
30- ref = { ref }
31- className = { cn (
32- "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap border-r border-charcoal-700 px-2 text-sm transition-all first:pl-0 last:border-none data-[state=active]:text-indigo-500 data-[state=inactive]:text-text-dimmed data-[state=inactive]:hover:text-text-bright focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" ,
33- className
34- ) }
35- { ...props }
36- />
37- ) ) ;
86+ React . ComponentPropsWithoutRef < typeof TabsPrimitive . Trigger > & {
87+ variant ?: Variants ;
88+ layoutId ?: string ;
89+ }
90+ > ( ( { className, variant = "pipe-divider" , layoutId, children, ...props } , ref ) => {
91+ const context = useClientTabsContext ( ) ;
92+ const activeValue = context ?. value ;
93+ const isActive = activeValue === props . value ;
94+
95+ if ( variant === "segmented" ) {
96+ return (
97+ < TabsPrimitive . Trigger
98+ ref = { ref }
99+ className = { cn (
100+ "group relative flex h-full grow items-center justify-center focus-custom disabled:pointer-events-none disabled:opacity-50" ,
101+ "flex-1 basis-0" ,
102+ className
103+ ) }
104+ { ...props }
105+ >
106+ < div className = "relative z-10 flex h-full w-full items-center justify-center px-3 py-[0.13rem]" >
107+ < span
108+ className = { cn (
109+ "text-sm transition duration-200" ,
110+ isActive ? "text-text-bright" : "text-text-dimmed transition hover:text-text-bright"
111+ ) }
112+ >
113+ { children }
114+ </ span >
115+ </ div >
116+ { isActive ? (
117+ layoutId ? (
118+ < motion . div
119+ layoutId = { layoutId }
120+ transition = { { duration : 0.4 , type : "spring" } }
121+ className = "absolute inset-0 rounded-[2px] border border-charcoal-500/50 bg-charcoal-600"
122+ />
123+ ) : (
124+ < div className = "absolute inset-0 rounded-[2px] border border-charcoal-500/50 bg-charcoal-600" />
125+ )
126+ ) : null }
127+ </ TabsPrimitive . Trigger >
128+ ) ;
129+ }
130+
131+ if ( variant === "underline" ) {
132+ return (
133+ < TabsPrimitive . Trigger
134+ ref = { ref }
135+ className = { cn (
136+ "group flex flex-col items-center pt-1 focus-custom disabled:pointer-events-none disabled:opacity-50" ,
137+ className
138+ ) }
139+ { ...props }
140+ >
141+ < span
142+ className = { cn (
143+ "text-sm transition duration-200" ,
144+ isActive ? "text-text-bright" : "text-text-dimmed hover:text-text-bright"
145+ ) }
146+ >
147+ { children }
148+ </ span >
149+ { layoutId ? (
150+ isActive ? (
151+ < motion . div
152+ layoutId = { layoutId }
153+ transition = { { type : "spring" , stiffness : 500 , damping : 30 } }
154+ className = "mt-1 h-0.5 w-full bg-indigo-500"
155+ />
156+ ) : (
157+ < div className = "mt-1 h-0.5 w-full bg-charcoal-500 opacity-0 transition duration-200 group-hover:opacity-100" />
158+ )
159+ ) : isActive ? (
160+ < div className = "mt-1 h-0.5 w-full bg-indigo-500" />
161+ ) : (
162+ < div className = "mt-1 h-0.5 w-full bg-charcoal-500 opacity-0 transition duration-200 group-hover:opacity-100" />
163+ ) }
164+ </ TabsPrimitive . Trigger >
165+ ) ;
166+ }
167+
168+ return (
169+ < TabsPrimitive . Trigger
170+ ref = { ref }
171+ className = { cn (
172+ "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap border-r border-charcoal-700 px-2 text-sm transition-all first:pl-0 last:border-none data-[state=active]:text-indigo-500 data-[state=inactive]:text-text-dimmed data-[state=inactive]:hover:text-text-bright focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" ,
173+ className
174+ ) }
175+ { ...props }
176+ >
177+ { children }
178+ </ TabsPrimitive . Trigger >
179+ ) ;
180+ } ) ;
38181ClientTabsTrigger . displayName = TabsPrimitive . Trigger . displayName ;
39182
40183const ClientTabsContent = React . forwardRef <
@@ -60,6 +203,7 @@ export type TabsProps = {
60203 currentValue : string ;
61204 className ?: string ;
62205 layoutId : string ;
206+ variant ?: Variants ;
63207} ;
64208
65209export { ClientTabs , ClientTabsContent , ClientTabsList , ClientTabsTrigger } ;
0 commit comments