21 lines
7.9 KiB
JSON
21 lines
7.9 KiB
JSON
{
|
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
"name": "tabs",
|
|
"type": "registry:ui",
|
|
"title": "Tabs",
|
|
"description": "A set of layered sections with the same height of content—known as tab panels—that are displayed one at a time.",
|
|
"dependencies": [
|
|
"motion"
|
|
],
|
|
"registryDependencies": [
|
|
"https://animate-ui.com/r/motion-highlight"
|
|
],
|
|
"files": [
|
|
{
|
|
"path": "registry/components/tabs/index.tsx",
|
|
"content": "'use client';\n\nimport * as React from 'react';\nimport { motion, type Transition, type HTMLMotionProps } from 'motion/react';\n\nimport { cn } from '@/lib/utils';\nimport {\n MotionHighlight,\n MotionHighlightItem,\n} from '@/components/animate-ui/effects/motion-highlight';\n\ntype TabsContextType<T extends string> = {\n activeValue: T;\n handleValueChange: (value: T) => void;\n registerTrigger: (value: T, node: HTMLElement | null) => void;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst TabsContext = React.createContext<TabsContextType<any> | undefined>(\n undefined,\n);\n\nfunction useTabs<T extends string = string>(): TabsContextType<T> {\n const context = React.useContext(TabsContext);\n if (!context) {\n throw new Error('useTabs must be used within a TabsProvider');\n }\n return context;\n}\n\ntype BaseTabsProps = React.ComponentProps<'div'> & {\n children: React.ReactNode;\n};\n\ntype UnControlledTabsProps<T extends string = string> = BaseTabsProps & {\n defaultValue?: T;\n value?: never;\n onValueChange?: never;\n};\n\ntype ControlledTabsProps<T extends string = string> = BaseTabsProps & {\n value: T;\n onValueChange?: (value: T) => void;\n defaultValue?: never;\n};\n\ntype TabsProps<T extends string = string> =\n | UnControlledTabsProps<T>\n | ControlledTabsProps<T>;\n\nfunction Tabs<T extends string = string>({\n defaultValue,\n value,\n onValueChange,\n children,\n className,\n ...props\n}: TabsProps<T>) {\n const [activeValue, setActiveValue] = React.useState<T | undefined>(\n defaultValue ?? undefined,\n );\n const triggersRef = React.useRef(new Map<string, HTMLElement>());\n const initialSet = React.useRef(false);\n const isControlled = value !== undefined;\n\n React.useEffect(() => {\n if (\n !isControlled &&\n activeValue === undefined &&\n triggersRef.current.size > 0 &&\n !initialSet.current\n ) {\n const firstTab = Array.from(triggersRef.current.keys())[0];\n setActiveValue(firstTab as T);\n initialSet.current = true;\n }\n }, [activeValue, isControlled]);\n\n const registerTrigger = (value: string, node: HTMLElement | null) => {\n if (node) {\n triggersRef.current.set(value, node);\n if (!isControlled && activeValue === undefined && !initialSet.current) {\n setActiveValue(value as T);\n initialSet.current = true;\n }\n } else {\n triggersRef.current.delete(value);\n }\n };\n\n const handleValueChange = (val: T) => {\n if (!isControlled) setActiveValue(val);\n else onValueChange?.(val);\n };\n\n return (\n <TabsContext.Provider\n value={{\n activeValue: (value ?? activeValue)!,\n handleValueChange,\n registerTrigger,\n }}\n >\n <div\n data-slot=\"tabs\"\n className={cn('flex flex-col gap-2', className)}\n {...props}\n >\n {children}\n </div>\n </TabsContext.Provider>\n );\n}\n\ntype TabsListProps = React.ComponentProps<'div'> & {\n children: React.ReactNode;\n activeClassName?: string;\n transition?: Transition;\n};\n\nfunction TabsList({\n children,\n className,\n activeClassName,\n transition = {\n type: 'spring',\n stiffness: 200,\n damping: 25,\n },\n ...props\n}: TabsListProps) {\n const { activeValue } = useTabs();\n\n return (\n <MotionHighlight\n controlledItems\n className={cn('rounded-sm bg-background shadow-sm', activeClassName)}\n value={activeValue}\n transition={transition}\n >\n <div\n role=\"tablist\"\n data-slot=\"tabs-list\"\n className={cn(\n 'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-[4px]',\n className,\n )}\n {...props}\n >\n {children}\n </div>\n </MotionHighlight>\n );\n}\n\ntype TabsTriggerProps = HTMLMotionProps<'button'> & {\n value: string;\n children: React.ReactNode;\n};\n\nfunction TabsTrigger({\n ref,\n value,\n children,\n className,\n ...props\n}: TabsTriggerProps) {\n const { activeValue, handleValueChange, registerTrigger } = useTabs();\n\n const localRef = React.useRef<HTMLButtonElement | null>(null);\n React.useImperativeHandle(ref, () => localRef.current as HTMLButtonElement);\n\n React.useEffect(() => {\n registerTrigger(value, localRef.current);\n return () => registerTrigger(value, null);\n }, [value, registerTrigger]);\n\n return (\n <MotionHighlightItem value={value} className=\"size-full\">\n <motion.button\n ref={localRef}\n data-slot=\"tabs-trigger\"\n role=\"tab\"\n whileTap={{ scale: 0.95 }}\n onClick={() => handleValueChange(value)}\n data-state={activeValue === value ? 'active' : 'inactive'}\n className={cn(\n 'inline-flex cursor-pointer items-center size-full justify-center whitespace-nowrap rounded-sm px-2 py-1 text-sm font-medium ring-offset-background transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground z-[1]',\n className,\n )}\n {...props}\n >\n {children}\n </motion.button>\n </MotionHighlightItem>\n );\n}\n\ntype TabsContentsProps = React.ComponentProps<'div'> & {\n children: React.ReactNode;\n transition?: Transition;\n};\n\nfunction TabsContents({\n children,\n className,\n transition = {\n type: 'spring',\n stiffness: 300,\n damping: 30,\n bounce: 0,\n restDelta: 0.01,\n },\n ...props\n}: TabsContentsProps) {\n const { activeValue } = useTabs();\n const childrenArray = React.Children.toArray(children);\n const activeIndex = childrenArray.findIndex(\n (child): child is React.ReactElement<{ value: string }> =>\n React.isValidElement(child) &&\n typeof child.props === 'object' &&\n child.props !== null &&\n 'value' in child.props &&\n child.props.value === activeValue,\n );\n\n return (\n <div\n data-slot=\"tabs-contents\"\n className={cn('overflow-hidden', className)}\n {...props}\n >\n <motion.div\n className=\"flex -mx-2\"\n animate={{ x: activeIndex * -100 + '%' }}\n transition={transition}\n >\n {childrenArray.map((child, index) => (\n <div key={index} className=\"w-full shrink-0 px-2\">\n {child}\n </div>\n ))}\n </motion.div>\n </div>\n );\n}\n\ntype TabsContentProps = HTMLMotionProps<'div'> & {\n value: string;\n children: React.ReactNode;\n};\n\nfunction TabsContent({\n children,\n value,\n className,\n ...props\n}: TabsContentProps) {\n const { activeValue } = useTabs();\n const isActive = activeValue === value;\n return (\n <motion.div\n role=\"tabpanel\"\n data-slot=\"tabs-content\"\n className={cn('overflow-hidden', className)}\n initial={{ filter: 'blur(0px)' }}\n animate={{ filter: isActive ? 'blur(0px)' : 'blur(4px)' }}\n exit={{ filter: 'blur(0px)' }}\n transition={{ type: 'spring', stiffness: 200, damping: 25 }}\n {...props}\n >\n {children}\n </motion.div>\n );\n}\n\nexport {\n Tabs,\n TabsList,\n TabsTrigger,\n TabsContents,\n TabsContent,\n useTabs,\n type TabsContextType,\n type TabsProps,\n type TabsListProps,\n type TabsTriggerProps,\n type TabsContentsProps,\n type TabsContentProps,\n};\n",
|
|
"type": "registry:ui",
|
|
"target": "components/animate-ui/components/tabs.tsx"
|
|
}
|
|
]
|
|
} |