Todos os componentes

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.

Abrir no catálogo

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

./components/ui/tab.tsx
'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';
./registry/components/tabs/preview-tab.tsx
'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;

Componentes parecidos