11"use client" ;
22
3- import { Tab , TabGroup , TabList , TabPanel , TabPanels } from "@headlessui/react" ;
4- import { AnimatePresence , motion } from "framer-motion" ;
3+ import {
4+ Content as TabsContent ,
5+ List as TabsList ,
6+ Root as TabsRoot ,
7+ Trigger as TabsTrigger ,
8+ } from "@radix-ui/react-tabs" ;
9+ import { motion } from "framer-motion" ;
510import {
611 Children ,
712 type ComponentType ,
@@ -83,14 +88,28 @@ export const CodeTabs = ({
8388 onChange ?: ( index : number ) => void ;
8489 className ?: string ;
8590} ) => {
86- const [ selectedIndex , setSelectedIndex ] = useState ( defaultIndex ) ;
91+ const childrenArray = Children . toArray ( children ) ;
92+ const validChildren = childrenArray . filter ( isValidElement ) ;
93+
94+ const defaultLabel = validChildren [ defaultIndex ]
95+ ? ( validChildren [ defaultIndex ] . props as { label : string } ) . label
96+ : undefined ;
8797
88- const handleChange = useCallback (
89- ( index : number ) => {
90- setSelectedIndex ( index ) ;
91- onChange ?.( index ) ;
98+ const [ selectedValue , setSelectedValue ] = useState ( defaultLabel ) ;
99+
100+ const handleValueChange = useCallback (
101+ ( value : string ) => {
102+ setSelectedValue ( value ) ;
103+ if ( onChange ) {
104+ const index = validChildren . findIndex (
105+ ( child ) => ( child . props as { label : string } ) . label === value
106+ ) ;
107+ if ( index !== - 1 ) {
108+ onChange ( index ) ;
109+ }
110+ }
92111 } ,
93- [ onChange ]
112+ [ onChange , validChildren ]
94113 ) ;
95114
96115 return (
@@ -100,72 +119,62 @@ export const CodeTabs = ({
100119 className
101120 ) }
102121 >
103- < TabGroup onChange = { handleChange } selectedIndex = { selectedIndex } >
104- < TabList
122+ < TabsRoot value = { selectedValue } onValueChange = { handleValueChange } defaultValue = { defaultLabel } >
123+ < TabsList
105124 aria-label = "Code language selection"
106125 className = "flex space-x-1 border-gray-200 border-b bg-gray-50/50 p-2 dark:border-gray-800 dark:bg-gray-900/50"
107126 >
108- { Children . map ( children , ( child ) => {
109- if ( ! isValidElement ( child ) ) {
110- return null ;
111- }
127+ { validChildren . map ( ( child ) => {
112128 const { label, disabled } = child . props as { label : string ; disabled ?: boolean } ;
113129 return (
114- < Tab
115- className = { ( { selected } ) =>
116- cn (
117- "relative cursor-pointer rounded-lg px-4 py-2 font-medium text-sm outline-none transition-all duration-200" ,
118- "disabled:cursor-not-allowed disabled:opacity-50" ,
119- selected
120- ? "bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400"
121- : "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
122- )
123- }
130+ < TabsTrigger
124131 disabled = { disabled }
125132 key = { label }
126- >
127- { ( ) => (
128- < span className = "flex items-center gap-2" >
129- < LanguageIcon label = { label } />
130- { label }
131- </ span >
133+ value = { label }
134+ className = { cn (
135+ "relative cursor-pointer rounded-lg px-4 py-2 font-medium text-sm outline-none transition-all duration-200" ,
136+ "disabled:cursor-not-allowed disabled:opacity-50" ,
137+ "data-[state=active]:bg-blue-50 data-[state=active]:text-blue-600 dark:data-[state=active]:bg-blue-900/20 dark:data-[state=active]:text-blue-400" ,
138+ "data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:bg-gray-100 data-[state=inactive]:hover:text-gray-900 dark:data-[state=inactive]:text-gray-400 dark:data-[state=inactive]:hover:bg-gray-800 dark:data-[state=inactive]:hover:text-gray-200"
132139 ) }
133- </ Tab >
140+ >
141+ < span className = "flex items-center gap-2" >
142+ < LanguageIcon label = { label } />
143+ { label }
144+ </ span >
145+ </ TabsTrigger >
134146 ) ;
135147 } ) }
136- </ TabList >
137- < TabPanels >
138- < AnimatePresence mode = "wait" >
139- { Children . map ( children , ( child ) => {
140- if ( ! isValidElement ( child ) ) {
141- return null ;
142- }
143- const { label, children : tabChildren } = child . props as {
144- label : string ;
145- children : ReactNode ;
146- } ;
147- return (
148- < TabPanel
149- className = { cn (
150- "p-4 ring-white/60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2" ,
151- "bg-gray-50 dark:bg-black/20"
152- ) }
153- key = { label }
154- >
155- < motion . div
156- animate = { { opacity : 1 , x : 0 } }
157- exit = { { opacity : 0 , x : 10 } }
158- initial = { { opacity : 0 , x : - 10 } }
159- transition = { { duration : 0.2 } }
160- >
161- { tabChildren }
162- </ motion . div >
163- </ TabPanel >
164- ) ;
165- } ) }
166- </ AnimatePresence >
167- </ TabPanels >
168- </ TabGroup >
148+ </ TabsList >
149+
150+ { validChildren . map ( ( child ) => {
151+ const { label, children : tabChildren } = child . props as {
152+ label : string ;
153+ children : ReactNode ;
154+ } ;
155+ return (
156+ < TabsContent
157+ className = { cn (
158+ "p-4 ring-white/60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2" ,
159+ "bg-gray-50 dark:bg-black/20"
160+ ) }
161+ key = { label }
162+ value = { label }
163+ >
164+ < motion . div
165+ animate = { { opacity : 1 , x : 0 } }
166+ // Exit animation is removed because Radix unmounts content immediately.
167+ // To support exit animations, we would need to control rendering manually outside of Tabs.Content
168+ // or use forceMount with standard AnimatePresence, but simple entry animation is usually sufficient.
169+ initial = { { opacity : 0 , x : - 10 } }
170+ transition = { { duration : 0.2 } }
171+ >
172+ { tabChildren }
173+ </ motion . div >
174+ </ TabsContent >
175+ ) ;
176+ } ) }
177+ </ TabsRoot >
169178 </ div >
170179 ) ;
171180} ;
0 commit comments