Creative Tab
Creative Tab é um componente de navegação para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.
UI Layouts
MIT
navigation
O que é
Creative Tab é um componente de navegação para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion
Licença
Este componente vem de UI Layouts e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Creative Tab
'use client';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'motion/react';
import React, {
createContext,
isValidElement,
type ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
// Improved TypeScript interfaces with more specific types
interface TabContextType {
activeTab: string;
setActiveTab: (value: string) => void;
wobbly: boolean;
hover: boolean;
defaultValue: string;
prevIndex: number;
setPrevIndex: (value: number) => void;
tabsOrder: string[];
}
const TabContext = createContext<TabContextType | undefined>(undefined);
// Custom hook with memoization
export const useTabs = () => {
const context = useContext(TabContext);
if (!context) {
throw new Error('useTabs must be used within a TabsProvider');
}
return context;
};
// Props interfaces with more specific types
interface TabsProviderProps {
children: ReactNode;
defaultValue: string;
wobbly?: boolean;
hover?: boolean;
}
interface TabsBtnProps {
children: ReactNode;
className?: string;
value: string;
}
interface TabsContentProps {
children: ReactNode;
className?: string;
value: string;
yValue?: boolean;
}
export const TabsProvider: React.FC<TabsProviderProps> = React.memo(
({ children, defaultValue, wobbly = true, hover = false }) => {
// Use useCallback to memoize state setters
const [activeTab, setActiveTab] = useState(defaultValue);
const [prevIndex, setPrevIndex] = useState(0);
// Memoize tabs order to prevent unnecessary recalculations
const tabsOrder = useMemo(() => {
return React.Children.toArray(children)
.filter((child) => isValidElement(child) && child.type === TabsContent)
.map((child) => (child as React.ReactElement<any>).props.value);
}, [children]);
// Memoize context value to prevent unnecessary re-renders
const contextValue = useMemo(
() => ({
activeTab,
setActiveTab,
wobbly,
hover,
defaultValue,
setPrevIndex,
prevIndex,
tabsOrder,
}),
[activeTab, setActiveTab, wobbly, hover, defaultValue, prevIndex, tabsOrder]
);
return <TabContext.Provider value={contextValue}>{children}</TabContext.Provider>;
}
);
// Memoized TabsBtn component
export const TabsBtn: React.FC<TabsBtnProps> = React.memo(({ children, className, value }) => {
const { activeTab, setPrevIndex, setActiveTab, defaultValue, hover, wobbly, tabsOrder } =
useTabs();
// Use useCallback to memoize the click handler
const handleClick = useCallback(() => {
setPrevIndex(tabsOrder.indexOf(activeTab));
setActiveTab(value);
}, [setPrevIndex, tabsOrder, activeTab, setActiveTab, value]);
return (
<motion.div
className={cn(`cursor-pointer 2xl:p-2 p-2 2xl:px-4 px-2 rounded-md relative`, className)}
onFocus={() => hover && handleClick()}
onMouseEnter={() => hover && handleClick()}
onClick={handleClick}
>
{children}
<AnimatePresence mode='wait'>
{activeTab === value && (
<>
<motion.div
transition={{
layout: {
duration: 0.2,
ease: 'easeInOut',
delay: 0.2,
},
}}
layoutId={defaultValue}
className='absolute w-full h-full left-0 top-0 dark:bg-primary-base bg-white rounded-md z-1'
/>
{wobbly && (
<>
<motion.div
transition={{
layout: {
duration: 0.4,
ease: 'easeInOut',
delay: 0.04,
},
}}
layoutId={defaultValue}
className='absolute w-full h-full left-0 top-0 dark:bg-primary-base bg-white rounded-md z-1 tab-shadow'
/>
<motion.div
transition={{
layout: {
duration: 0.4,
ease: 'easeOut',
delay: 0.2,
},
}}
layoutId={`${defaultValue}b`}
className='absolute w-full h-full left-0 top-0 dark:bg-primary-base bg-white rounded-md z-1 tab-shadow'
/>
</>
)}
</>
)}
</AnimatePresence>
</motion.div>
);
});
// Memoized TabsContent component
export const TabsContent: React.FC<TabsContentProps> = React.memo(
({ children, className, value, yValue }) => {
const { activeTab, tabsOrder, prevIndex } = useTabs();
// Memoize direction calculation
const isForward = useMemo(
() => tabsOrder.indexOf(activeTab) > prevIndex,
[tabsOrder, activeTab, prevIndex]
);
return (
<AnimatePresence mode='popLayout'>
{activeTab === value && (
<motion.div
initial={{ opacity: 0, y: yValue ? (isForward ? 10 : -10) : 0 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: yValue ? (isForward ? -50 : 50) : 0 }}
transition={{
duration: 0.3,
ease: 'easeInOut',
delay: 0.5,
}}
className={cn('p-2 px-4 rounded-md relative', className)}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
}
);
// Add display names for better debugging
TabsProvider.displayName = 'TabsProvider';
TabsBtn.displayName = 'TabsBtn';
TabsContent.displayName = 'TabsContent';
// thanks to maximeheckel
import preview from '@/assets/preview/Preview';
import { TabsBtn, TabsContent, TabsProvider } from '@/components/ui/tab';
import Image from 'next/image';
import React, { useState } from 'react';
function HomeTab() {
return (
<>
<TabsProvider defaultValue={'accordion'} wobbly={true}>
<div className='flex justify-center pt-4'>
<div className='flex items-center capitalize w-fit dark:bg-[#1a1c20] bg-neutral-300 p-1 dark:text-white text-black rounded-md border'>
<TabsBtn value='accordion'>
<span className='relative z-2 '>Accordion</span>
</TabsBtn>
<TabsBtn value='globe'>
<span className='relative z-2 '>Globe</span>
</TabsBtn>
<TabsBtn value='mousetrail'>
<span className='relative z-2 '>Mouse Trail</span>
</TabsBtn>
</div>
</div>
<TabsContent value='accordion' yValue={false}>
<div className='dark:bg-black bg-white mx-auto max-w-[450px] lg:h-[300px] md:h-[240px] h-[190px] dark:text-white text-black p-4 border shadow-md rounded-md space-y-2'>
<Image
className='rounded-xl h-full object-contain'
src={preview.galleryNew}
width={600}
height={680}
alt={'gallery'}
/>
</div>
</TabsContent>
<TabsContent value='globe' yValue={false}>
<div className='dark:bg-black bg-white mx-auto max-w-[450px] lg:h-[300px] md:h-[240px] h-[190px] dark:text-white text-black p-4 border shadow-md rounded-md space-y-2'>
<Image
className='rounded-xl h-full object-contain'
src={preview.globe}
width={600}
height={680}
alt={'globe'}
/>
</div>
</TabsContent>
<TabsContent value='mousetrail' yValue={false}>
<div className='dark:bg-black bg-white mx-auto max-w-[450px] lg:h-[300px] md:h-[240px] h-[190px] dark:text-white text-black p-4 border shadow-md rounded-md space-y-2'>
<Image
className='rounded-xl h-full object-contain'
src={preview.mousetrail}
width={600}
height={680}
alt={'mousetrail'}
/>
</div>
</TabsContent>
</TabsProvider>
</>
);
}
export default HomeTab;