finalizada sección de Referencias para la IA
This commit is contained in:
261
src/components/ui/motion-tabs.tsx
Normal file
261
src/components/ui/motion-tabs.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
import { motion, type Transition, type HTMLMotionProps } from 'motion/react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { MotionHighlight, MotionHighlightItem } from '@/components/ui/motion-highlight'
|
||||
|
||||
type TabsContextType<T extends string> = {
|
||||
activeValue: T
|
||||
handleValueChange: (value: T) => void
|
||||
registerTrigger: (value: T, node: HTMLElement | null) => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const TabsContext = React.createContext<TabsContextType<any> | undefined>(undefined)
|
||||
|
||||
function useTabs<T extends string = string>(): TabsContextType<T> {
|
||||
const context = React.useContext(TabsContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useTabs must be used within a TabsProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
type BaseTabsProps = React.ComponentProps<'div'> & {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
type UnControlledTabsProps<T extends string = string> = BaseTabsProps & {
|
||||
defaultValue?: T
|
||||
value?: never
|
||||
onValueChange?: never
|
||||
}
|
||||
|
||||
type ControlledTabsProps<T extends string = string> = BaseTabsProps & {
|
||||
value: T
|
||||
onValueChange?: (value: T) => void
|
||||
defaultValue?: never
|
||||
}
|
||||
|
||||
type TabsProps<T extends string = string> = UnControlledTabsProps<T> | ControlledTabsProps<T>
|
||||
|
||||
function Tabs<T extends string = string>({
|
||||
defaultValue,
|
||||
value,
|
||||
onValueChange,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: TabsProps<T>) {
|
||||
const [activeValue, setActiveValue] = React.useState<T | undefined>(defaultValue ?? undefined)
|
||||
const triggersRef = React.useRef(new Map<string, HTMLElement>())
|
||||
const initialSet = React.useRef(false)
|
||||
const isControlled = value !== undefined
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isControlled && activeValue === undefined && triggersRef.current.size > 0 && !initialSet.current) {
|
||||
const firstTab = Array.from(triggersRef.current.keys())[0]
|
||||
|
||||
setActiveValue(firstTab as T)
|
||||
initialSet.current = true
|
||||
}
|
||||
}, [activeValue, isControlled])
|
||||
|
||||
const registerTrigger = (value: string, node: HTMLElement | null) => {
|
||||
if (node) {
|
||||
triggersRef.current.set(value, node)
|
||||
|
||||
if (!isControlled && activeValue === undefined && !initialSet.current) {
|
||||
setActiveValue(value as T)
|
||||
initialSet.current = true
|
||||
}
|
||||
} else {
|
||||
triggersRef.current.delete(value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleValueChange = (val: T) => {
|
||||
if (!isControlled) setActiveValue(val)
|
||||
else onValueChange?.(val)
|
||||
}
|
||||
|
||||
return (
|
||||
<TabsContext.Provider
|
||||
value={{
|
||||
activeValue: (value ?? activeValue)!,
|
||||
handleValueChange,
|
||||
registerTrigger
|
||||
}}
|
||||
>
|
||||
<div data-slot='tabs' className={cn('flex flex-col gap-2', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
</TabsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
type TabsListProps = React.ComponentProps<'div'> & {
|
||||
children: React.ReactNode
|
||||
activeClassName?: string
|
||||
transition?: Transition
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
children,
|
||||
className,
|
||||
activeClassName,
|
||||
transition = {
|
||||
type: 'spring',
|
||||
stiffness: 200,
|
||||
damping: 25
|
||||
},
|
||||
...props
|
||||
}: TabsListProps) {
|
||||
const { activeValue } = useTabs()
|
||||
|
||||
return (
|
||||
<MotionHighlight
|
||||
controlledItems
|
||||
className={cn('bg-background rounded-sm shadow-sm', activeClassName)}
|
||||
value={activeValue}
|
||||
transition={transition}
|
||||
>
|
||||
<div
|
||||
role='tablist'
|
||||
data-slot='tabs-list'
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-[4px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</MotionHighlight>
|
||||
)
|
||||
}
|
||||
|
||||
type TabsTriggerProps = HTMLMotionProps<'button'> & {
|
||||
value: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function TabsTrigger({ ref, value, children, className, ...props }: TabsTriggerProps) {
|
||||
const { activeValue, handleValueChange, registerTrigger } = useTabs()
|
||||
|
||||
const localRef = React.useRef<HTMLButtonElement | null>(null)
|
||||
|
||||
React.useImperativeHandle(ref, () => localRef.current as HTMLButtonElement)
|
||||
|
||||
React.useEffect(() => {
|
||||
registerTrigger(value, localRef.current)
|
||||
|
||||
return () => registerTrigger(value, null)
|
||||
}, [value, registerTrigger])
|
||||
|
||||
return (
|
||||
<MotionHighlightItem value={value} className='size-full'>
|
||||
<motion.button
|
||||
ref={localRef}
|
||||
data-slot='tabs-trigger'
|
||||
role='tab'
|
||||
onClick={() => handleValueChange(value)}
|
||||
data-state={activeValue === value ? 'active' : 'inactive'}
|
||||
className={cn(
|
||||
'ring-offset-background focus-visible:ring-ring data-[state=active]:text-foreground z-[1] inline-flex size-full cursor-pointer items-center justify-center rounded-sm px-2 py-1 text-sm font-medium whitespace-nowrap transition-transform focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.button>
|
||||
</MotionHighlightItem>
|
||||
)
|
||||
}
|
||||
|
||||
type TabsContentsProps = React.ComponentProps<'div'> & {
|
||||
children: React.ReactNode
|
||||
transition?: Transition
|
||||
}
|
||||
|
||||
function TabsContents({
|
||||
children,
|
||||
className,
|
||||
transition = {
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
bounce: 0,
|
||||
restDelta: 0.01
|
||||
},
|
||||
...props
|
||||
}: TabsContentsProps) {
|
||||
const { activeValue } = useTabs()
|
||||
const childrenArray = React.Children.toArray(children)
|
||||
|
||||
const activeIndex = childrenArray.findIndex(
|
||||
(child): child is React.ReactElement<{ value: string }> =>
|
||||
React.isValidElement(child) &&
|
||||
typeof child.props === 'object' &&
|
||||
child.props !== null &&
|
||||
'value' in child.props &&
|
||||
child.props.value === activeValue
|
||||
)
|
||||
|
||||
return (
|
||||
<div data-slot='tabs-contents' className={cn('overflow-hidden', className)} {...props}>
|
||||
<motion.div className='-mx-2 flex' animate={{ x: activeIndex * -100 + '%' }} transition={transition}>
|
||||
{childrenArray.map((child, index) => (
|
||||
<div key={index} className='w-full shrink-0 px-2'>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type TabsContentProps = HTMLMotionProps<'div'> & {
|
||||
value: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function TabsContent({ children, value, className, ...props }: TabsContentProps) {
|
||||
const { activeValue } = useTabs()
|
||||
const isActive = activeValue === value
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
role='tabpanel'
|
||||
data-slot='tabs-content'
|
||||
className={cn('overflow-hidden', className)}
|
||||
initial={{ filter: 'blur(0px)' }}
|
||||
animate={{ filter: isActive ? 'blur(0px)' : 'blur(2px)' }}
|
||||
exit={{ filter: 'blur(0px)' }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContents,
|
||||
TabsContent,
|
||||
useTabs,
|
||||
type TabsContextType,
|
||||
type TabsProps,
|
||||
type TabsListProps,
|
||||
type TabsTriggerProps,
|
||||
type TabsContentsProps,
|
||||
type TabsContentProps
|
||||
}
|
||||
Reference in New Issue
Block a user