Animated Tabs
Animated Tabs é 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 é
Animated Tabs é 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.
Animated Tabs
'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';
'use client';
import { TabsBtn, TabsContent, TabsProvider } from '@/components/ui/tab';
import { AnimatePresence, motion } from 'motion/react';
import Image from 'next/image';
import React, { useState } from 'react';
const effectArr = [
{
id: 1,
name: 'basic',
},
{
id: 2,
name: 'hover',
},
{
id: 3,
name: 'wobbly',
},
];
function PreviewTab() {
const [checkEffect, setCheckEffect] = useState('basic');
return (
<>
<div className='flex bg-black dark:bg-neutral-900 w-fit ml-auto mb-4 gap-1 p-1 rounded-md text-white'>
{effectArr?.map((effect, index) => {
return (
<>
<motion.button
onClick={() => setCheckEffect(effect?.name)}
className={`py-1 px-3 rounded-md capitalize relative `}
>
<span className='z-10 relative'>{effect?.name}</span>
{checkEffect === effect.name && (
<motion.div
transition={{
layout: {
duration: 0.2,
ease: 'easeInOut',
},
}}
layoutId={'magnetic'}
className='absolute w-full h-full left-0 top-0 bg-[conic-gradient(from_90deg_at_50%_50%,#8494ff_0%,#3749be_50%,#7d8efc_100%)] rounded-md z-1 tab-shadow'
/>
)}
</motion.button>
</>
);
})}
</div>
<div className='border bg-white/10 dark:bg-black/40 backdrop-blur-xs rounded-md p-4 relative'>
<TabsProvider
defaultValue={'design'}
wobbly={checkEffect === 'wobbly' ? true : false}
hover={checkEffect === 'hover' ? true : false}
>
<div className='flex justify-center mt-2'>
<div className='flex items-center w-fit dark:bg-[#1d2025] bg-neutral-200 p-1 dark:text-white text-black rounded-md border'>
<TabsBtn value='design'>
<span className='relative z-2 uppercase sm:text-base text-xs'>design</span>
</TabsBtn>
<TabsBtn value='collaborate'>
<span className='relative z-2 uppercase sm:text-base text-xs'>collaborate</span>
</TabsBtn>
<TabsBtn value='share'>
<span className='relative z-2 uppercase sm:text-base text-xs'>share</span>
</TabsBtn>
<TabsBtn value='publish'>
<span className='relative z-2 uppercase sm:text-base text-xs'>publish</span>
</TabsBtn>
</div>
</div>
<TabsContent value='design'>
<div className='w-full'>
<Image
src={
'https://images.unsplash.com/photo-1506097425191-7ad538b29cef?q=80&w=1000&auto=format&fit=crop'
}
width={1000}
height={1000}
alt='preview_img'
className='w-[850px] object-cover h-full mx-auto rounded-md'
/>
</div>
</TabsContent>
<TabsContent value='collaborate'>
<div className='w-full'>
<Image
src={
'https://images.unsplash.com/photo-1557804506-669a67965ba0?q=80&w=1000&auto=format&fit=crop'
}
width={1000}
height={1000}
alt='preview_img'
className='w-[850px] object-cover h-full mx-auto rounded-md'
/>
</div>
</TabsContent>
<TabsContent value='share'>
<div className='w-full'>
<Image
src={
'https://images.unsplash.com/photo-1665470909901-162912ec16f7?q=80&w=1000&auto=format&fit=crop'
}
width={1000}
height={1000}
alt='preview_img'
className='w-[850px] object-cover h-full mx-auto rounded-md'
/>
</div>
</TabsContent>
<TabsContent value='publish'>
<div className='w-full'>
<Image
src={
'https://images.unsplash.com/photo-1694022861804-840f61d1c452?q=80&w=1000&auto=format&fit=crop'
}
width={1000}
height={1000}
alt='preview_img'
className='w-[850px] object-cover h-full mx-auto rounded-md'
/>
</div>
</TabsContent>
</TabsProvider>
</div>
</>
);
}
export default PreviewTab;