Todos os componentes

Bottom Directional Drawer

Bottom Directional Drawer é um componente de sobreposição para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.

UI Layouts MIT overlays

O que é

Bottom Directional Drawer é um componente de sobreposiçã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 lucide-react

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.

Bottom Directional Drawer

./components/ui/directional-drawer.tsx
'use client';
import { cn } from '@/lib/utils';
import { X } from 'lucide-react';
import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react';
import { Drawer as VaulSidebar } from 'vaul';

interface DrawerContextProps {
  open: boolean;
  setOpen: (open: boolean) => void;
}

const DrawerContext = createContext<DrawerContextProps | undefined>(undefined);

export const useDirectionalDrawer = () => {
  const context = useContext(DrawerContext);
  if (!context) {
    throw new Error('useDirectionalDrawer must be used within a DirectionalDrawer');
  }
  return context;
};

interface DirectionalDrawerProps {
  children: ReactNode;
  open?: boolean;
  setOpen?: (open: boolean) => void;
  direction?: 'left' | 'right' | 'top' | 'bottom';
  outsideClose?: boolean;
  className?: string;
}

export function DirectionalDrawer({
  children,
  open: controlledOpen,
  setOpen: controlledSetOpen,
  direction = 'left',
  outsideClose = true,
  className,
}: DirectionalDrawerProps) {
  const [internalOpen, setInternalOpen] = useState(false);
  const [isDesktop, setIsDesktop] = useState(false);

  const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
  const setOpen = controlledSetOpen || setInternalOpen;

  useEffect(() => {
    const mediaQuery = window.matchMedia('(min-width: 768px)');
    const handleMediaChange = (event: MediaQueryListEvent) => {
      setIsDesktop(event.matches);
    };

    setIsDesktop(mediaQuery.matches);
    mediaQuery.addEventListener('change', handleMediaChange);

    return () => {
      mediaQuery.removeEventListener('change', handleMediaChange);
    };
  }, []);

  const trigger = React.Children.toArray(children).find(
    (child: any) => child.type === DrawerTrigger
  );
  const content = React.Children.toArray(children).filter(
    (child: any) => child.type !== DrawerTrigger
  );

  // Helper function to get positioning and sizing classes
  const getDirectionClasses = () => {
    switch (direction) {
      case 'right':
        return {
          position: 'right-0 bottom-0',
          size: outsideClose ? 'sm:w-[450px] w-[90%] h-full' : 'w-full h-full',
          border: 'border-l',
          handlePosition: 'top-[40%] left-2',
          handleSize: 'h-16 w-[0.30rem]',
        };
      case 'top':
        return {
          position: 'top-0 left-0',
          size: outsideClose ? 'w-full sm:h-[450px] h-[90%]' : 'w-full h-full',
          border: 'border-b',
          handlePosition: 'bottom-2 left-[40%]',
          handleSize: 'w-16 h-[0.30rem]',
        };
      case 'bottom':
        return {
          position: 'bottom-0 left-0',
          size: outsideClose ? 'w-full sm:h-[450px] h-[90%]' : 'w-full h-full',
          border: 'border-t',
          handlePosition: 'top-2 left-[40%]',
          handleSize: 'w-16 h-[0.30rem]',
        };
      case 'left':
      default:
        return {
          position: 'left-0 bottom-0',
          size: outsideClose ? 'sm:w-[450px] w-[90%] h-full' : 'w-full h-full',
          border: 'border-r',
          handlePosition: 'top-[40%] right-2',
          handleSize: 'h-16 w-[0.30rem]',
        };
    }
  };

  const directionClasses = getDirectionClasses();
  const vaulDirection =
    direction === 'right'
      ? 'right'
      : direction === 'top'
        ? 'top'
        : direction === 'bottom'
          ? 'bottom'
          : 'left';

  return (
    <DrawerContext.Provider value={{ open, setOpen }}>
      {trigger}

      <VaulSidebar.Root
        open={open}
        direction={vaulDirection}
        onOpenChange={setOpen}
        dismissible={isDesktop ? false : true}
      >
        <VaulSidebar.Portal>
          <VaulSidebar.Overlay
            className='fixed inset-0 dark:bg-black/40 bg-white/50 backdrop-blur-xs z-50'
            onClick={() => setOpen(false)}
          />
          <VaulSidebar.Content
            className={cn(
              `${directionClasses.border} z-50 ${directionClasses.size} fixed ${directionClasses.position} ${
                outsideClose ? 'dark:bg-zinc-950 bg-zinc-100' : ''
              }`,
              className
            )}
          >
            <div
              className={`${
                outsideClose
                  ? 'w-full h-full'
                  : `dark:bg-neutral-900 relative bg-white ${directionClasses.border} ${directionClasses.size}`
              }`}
            >
              {isDesktop ? (
                <button
                  className='flex justify-end w-full absolute right-2 top-2'
                  onClick={() => setOpen(false)}
                >
                  <X />
                </button>
              ) : (
                <div
                  className={`absolute ${directionClasses.handlePosition} mx-auto ${directionClasses.handleSize} shrink-0 rounded-full bg-neutral-600 my-4`}
                />
              )}
              {content}
            </div>
          </VaulSidebar.Content>
        </VaulSidebar.Portal>
      </VaulSidebar.Root>
    </DrawerContext.Provider>
  );
}

export function DrawerContent({
  children,
  className,
}: {
  children: ReactNode;
  className?: string;
}) {
  return <div className={cn('', className)}>{children}</div>;
}

export function DrawerTrigger({ children }: { children: ReactNode }) {
  const { setOpen } = useDirectionalDrawer();
  return <div onClick={() => setOpen(true)}>{children}</div>;
}
./registry/components/drawer/bottom-directional-drawer.tsx
'use client';
import {
  DirectionalDrawer,
  DrawerContent,
  DrawerTrigger,
} from '@/components/ui/directional-drawer';
import { useMediaQuery } from '@/hooks/use-media-query';
import { Edit, X } from 'lucide-react';
import { motion } from 'motion/react';
import Image from 'next/image';
import { useState } from 'react';
import { Drawer } from 'vaul';
export default function BottomDirectionalDrawer() {
  const [sidebarOpen, setSidebarOpen] = useState(false);
  return (
    <>
      <DirectionalDrawer
        open={sidebarOpen}
        setOpen={setSidebarOpen}
        direction={'bottom'}
        outsideClose={true}
      >
        <DrawerContent className='w-full h-full flex justify-end'>
          <div className='p-5 rounded-t-md grow w-full pt-14'>
            <h1 className='font-medium  text-2xl'>Update Profile Image</h1>
            <p className='text-sm text-muted-foreground'>
              Upload a new profile image or remove the current one.
            </p>
            <div className='p-2 space-y-4 '>
              <span className='relative flex justify-center overflow-hidden rounded-xl w-full '>
                <span className='grid place-content-center h-40  w-40 rounded-xl dark:bg-neutral-800 bg-muted'>
                  JP
                </span>
              </span>
              <div className='mb-3'>
                <input
                  className='w-full border file:p-2 file:bg-black  file:border-none  file:text-white rounded-xs overflow-hidden'
                  type='file'
                  id='formFile'
                />
              </div>
              <button
                type='submit'
                className='w-full rounded-xs dark:bg-white bg-black  p-2 dark:text-black text-white'
              >
                Submit
              </button>
            </div>
          </div>
        </DrawerContent>
      </DirectionalDrawer>
      <div className='flex justify-center'>
        <figure className='h-96 w-96 relative'>
          <Image
            src={'/myself.webp'}
            width={600}
            height={600}
            className='h-full w-full object-cover rounded-lg '
            alt='profile_image'
          />
          <motion.button
            whileTap={{ scale: 0.8 }}
            onClick={() => setSidebarOpen(true)}
            className='absolute left-2 bottom-2 p-4 dark:bg-black bg-white rounded-lg shadow-black'
          >
            <Edit />
          </motion.button>
        </figure>
      </div>
    </>
  );
}

Componentes parecidos