{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "tabs", "type": "registry:ui", "title": "Tabs", "description": "A tabs component that can be used to switch between different views or sections.", "files": [ { "path": "registry/tra-kit/components/tabs.tsx", "content": "import React, { useState, useEffect, useRef, useContext, createContext, ReactNode } from 'react';\r\nimport { cva } from 'class-variance-authority';\r\nimport { cn } from '@/lib/utils';\r\n\r\ninterface ITabsContext {\r\n activeTab: string;\r\n handleTabClick: (value: string) => void;\r\n variant: 'default' | 'solid' | 'outlined' | 'split';\r\n}\r\n\r\nconst defaultTabsContext: ITabsContext = {\r\n activeTab: '',\r\n handleTabClick: () => {},\r\n variant: 'default',\r\n};\r\n\r\nconst TabsContext = createContext(defaultTabsContext);\r\n\r\nconst tabsContainerVariants = cva('flex h-fit gap-2', {\r\n variants: {\r\n contentPlacement: {\r\n top: 'flex-col-reverse',\r\n right: 'flex-row',\r\n bottom: 'flex-col',\r\n left: 'flex-row-reverse',\r\n },\r\n },\r\n defaultVariants: {\r\n contentPlacement: 'bottom',\r\n },\r\n});\r\n\r\nconst tabsVariants = cva('inline-flex w-fit items-center justify-center isolate', {\r\n variants: {\r\n variant: {\r\n default: 'bg-transparent shadow-md rounded-none!',\r\n solid: 'bg-neutral-light p-1',\r\n outlined: 'border-2 bg-transparent p-1',\r\n split: 'bg-transparent',\r\n },\r\n radius: {\r\n default: 'rounded-md',\r\n none: 'rounded-none',\r\n sm: 'rounded-sm',\r\n lg: 'rounded-lg',\r\n full: 'rounded-full',\r\n },\r\n },\r\n defaultVariants: {\r\n variant: 'default',\r\n radius: 'default',\r\n },\r\n});\r\n\r\n// eslint-disable-next-line tailwindcss/no-custom-classname\r\nconst tabVariants = cva(\r\n 'select-none font-medium transition-all duration-300 disabled:pointer-events-none disabled:opacity-50',\r\n {\r\n variants: {\r\n isActive: {\r\n true: 'active-tab',\r\n false: '',\r\n },\r\n variant: {\r\n default: 'relative z-20',\r\n solid: 'relative z-20',\r\n outlined: 'relative z-20',\r\n split: 'relative z-20',\r\n },\r\n size: {\r\n sm: 'px-2 py-1 text-xs',\r\n default: 'px-4 py-2 text-sm',\r\n lg: 'px-6 py-4',\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n variant: 'default',\r\n isActive: true,\r\n // className: 'border-primary text-primary',\r\n className: 'text-primary hover:text-primary/80',\r\n },\r\n {\r\n variant: 'default',\r\n isActive: false,\r\n // className: 'border-neutral text-neutral-grey hover:brightness-75',\r\n className: 'text-neutral hover:text-neutral/80',\r\n },\r\n {\r\n variant: 'solid',\r\n isActive: true,\r\n className: 'text-neutral-black hover:text-neutral-black/80',\r\n },\r\n {\r\n variant: 'solid',\r\n isActive: false,\r\n className: 'text-neutral-black/90 hover:text-neutral-grey/60',\r\n },\r\n {\r\n variant: 'outlined',\r\n isActive: true,\r\n className: 'text-neutral-black hover:text-neutral-black/80',\r\n },\r\n {\r\n variant: 'outlined',\r\n isActive: false,\r\n className: 'text-neutral-black/90 hover:text-neutral-grey/60',\r\n },\r\n {\r\n variant: 'split',\r\n isActive: true,\r\n className: 'text-neutral-black hover:text-neutral-black/80',\r\n },\r\n {\r\n variant: 'split',\r\n isActive: false,\r\n className: 'text-neutral-black/90 hover:text-neutral-grey/60',\r\n },\r\n ],\r\n defaultVariants: {\r\n variant: 'default',\r\n isActive: false,\r\n size: 'default',\r\n },\r\n },\r\n);\r\n\r\nconst selectorVariants = cva('absolute transition-transform duration-200', {\r\n variants: {\r\n variant: {\r\n default: 'bg-primary z-30 rounded-md',\r\n solid: 'bg-neutral-white z-10 h-full',\r\n outlined: 'bg-neutral dark:bg-neutral-white z-10 h-full',\r\n split: 'bg-neutral dark:bg-neutral-white z-10 h-full',\r\n },\r\n disabled: {\r\n true: 'pointer-events-none opacity-50',\r\n false: '',\r\n },\r\n radius: {\r\n default: 'rounded-md',\r\n none: 'rounded-none',\r\n sm: 'rounded-sm',\r\n lg: 'rounded-lg',\r\n full: 'rounded-full',\r\n },\r\n direction: {\r\n horizontal: 'bottom-0',\r\n vertical: 'top-0',\r\n },\r\n },\r\n\r\n defaultVariants: {\r\n variant: 'default',\r\n disabled: false,\r\n radius: 'default',\r\n direction: 'horizontal',\r\n },\r\n\r\n compoundVariants: [\r\n {\r\n variant: 'default',\r\n direction: 'vertical',\r\n className: '-left-[1.5px]!',\r\n },\r\n {\r\n variant: 'default',\r\n direction: 'horizontal',\r\n className: '-bottom-[1.5px]!',\r\n },\r\n ],\r\n});\r\n\r\ninterface ITabsBase {\r\n variant?: 'default' | 'solid' | 'outlined' | 'split';\r\n className?: string;\r\n children: ReactNode;\r\n disabled?: boolean;\r\n size?: 'default' | 'sm' | 'lg';\r\n radius?: 'default' | 'none' | 'sm' | 'lg' | 'full';\r\n selectorClassName?: string;\r\n direction?: 'horizontal' | 'vertical';\r\n contentPlacement?: 'top' | 'bottom' | 'left' | 'right';\r\n contentClasName?: string;\r\n wrapperClassName?: string;\r\n}\r\n\r\n// Controlled mode: activeTab and onChange are required\r\ninterface ITabsControlled extends ITabsBase {\r\n activeTab: string;\r\n onChange: (value: string) => void;\r\n defaultActiveTab?: never;\r\n}\r\n\r\n// Uncontrolled mode: activeTab and onChange are optional, defaultActiveTab can be used\r\ninterface ITabsUncontrolled extends ITabsBase {\r\n activeTab?: never;\r\n onChange?: (value: string) => void;\r\n defaultActiveTab?: string;\r\n}\r\n\r\ntype ITabs = ITabsControlled | ITabsUncontrolled;\r\n\r\ninterface ITab {\r\n label: string | ReactNode;\r\n value: string;\r\n isActive?: boolean;\r\n onClick?: () => void;\r\n className?: string;\r\n children: ReactNode;\r\n disabled?: boolean;\r\n allDisabled?: boolean;\r\n size?: 'default' | 'sm' | 'lg';\r\n radius?: 'default' | 'none' | 'sm' | 'lg' | 'full';\r\n}\r\n\r\nconst Tabs: React.FC = ({\r\n activeTab: externalActiveTab,\r\n variant = 'default',\r\n onChange,\r\n className,\r\n contentClasName,\r\n wrapperClassName,\r\n selectorClassName,\r\n children,\r\n disabled,\r\n size = 'default',\r\n radius = 'default',\r\n direction = 'horizontal',\r\n contentPlacement = 'bottom',\r\n defaultActiveTab,\r\n}) => {\r\n const [indicatorStyle, setIndicatorStyle] = useState({});\r\n const tabsRef = useRef(null);\r\n\r\n const [internalActiveTab, setInternalActiveTab] = useState(() => {\r\n if (defaultActiveTab) return defaultActiveTab;\r\n const firstTab = React.Children.toArray(children).find((child) =>\r\n React.isValidElement(child),\r\n ) as React.ReactElement | undefined;\r\n return firstTab?.props.value || '';\r\n });\r\n\r\n const isControlled = externalActiveTab !== undefined;\r\n const activeTab = isControlled ? externalActiveTab : internalActiveTab;\r\n\r\n useEffect(() => {\r\n const activeTabElement = tabsRef.current?.querySelector('.active-tab') as HTMLButtonElement;\r\n\r\n // if (activeTabElement && variant !== 'default') {\r\n if (activeTabElement) {\r\n const newIndicatorStyle = {\r\n width:\r\n variant === 'default' && direction === 'vertical'\r\n ? '2px'\r\n : direction === 'horizontal'\r\n ? activeTabElement.offsetWidth\r\n : '100%',\r\n height:\r\n variant === 'default' && direction === 'horizontal'\r\n ? '2px'\r\n : direction === 'vertical'\r\n ? activeTabElement.offsetHeight\r\n : '100%',\r\n // transform: direction === 'horizontal' ? `translateX(${activeTabElement.offsetLeft}px)` : `translateY(${-activeTabElement.offsetTop}px)`,\r\n transform:\r\n direction === 'horizontal'\r\n ? `translateX(${activeTabElement.offsetLeft}px)`\r\n : `translateY(${activeTabElement.offsetTop}px)`,\r\n };\r\n setIndicatorStyle(newIndicatorStyle);\r\n }\r\n }, [activeTab, direction, variant]);\r\n\r\n const handleTabClick = (value: string) => {\r\n if (isControlled) {\r\n if (onChange) onChange(value);\r\n } else {\r\n setInternalActiveTab(value);\r\n if (onChange) onChange(value);\r\n }\r\n };\r\n\r\n const contextValue = React.useMemo(\r\n () => ({ activeTab, handleTabClick, variant }),\r\n [activeTab, variant],\r\n );\r\n\r\n return (\r\n \r\n
\r\n
\r\n \r\n \r\n {React.Children.map(children, (child) => {\r\n if (React.isValidElement(child)) {\r\n return React.cloneElement(child, {\r\n isActive: child.props.value === activeTab,\r\n onClick: () => handleTabClick(child.props.value),\r\n allDisabled: disabled,\r\n size,\r\n });\r\n }\r\n return null;\r\n })}\r\n
\r\n
\r\n
\r\n {React.Children.map(children, (child) => {\r\n if (React.isValidElement(child) && child.props.value === activeTab) {\r\n return
{child.props.children}
;\r\n }\r\n return null;\r\n })}\r\n
\r\n \r\n
\r\n );\r\n};\r\n\r\nconst Tab: React.FC = ({ className, label, value, onClick, disabled, allDisabled, size }) => {\r\n const context = useContext(TabsContext);\r\n\r\n if (!context) {\r\n throw new Error('Tab component must be used within a Tabs component');\r\n }\r\n\r\n const { activeTab, handleTabClick } = context;\r\n const isActive = value === activeTab;\r\n\r\n return (\r\n handleTabClick(value))}\r\n >\r\n {label}\r\n \r\n );\r\n};\r\n\r\nexport { Tabs, Tab };\r\n", "type": "registry:ui" } ] }