Todos os componentes

Drawer

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

SmoothUI MIT overlays

O que é

Drawer é um componente de sobreposição para React e Tailwind CSS, da biblioteca SmoothUI, 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 SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Drawer

index.tsx
"use client";

import {
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  Drawer as DrawerPrimitive,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";

/* ------------------------------------------------------------------ */
/*  Animation constants                                                */
/* ------------------------------------------------------------------ */

const BACKDROP_DURATION = 0.2;
const STAGGER_BASE_DELAY = 0.12;
const STAGGER_CHILD_DELAY = 0.05;

/* ------------------------------------------------------------------ */
/*  Types                                                              */
/* ------------------------------------------------------------------ */

export type DrawerSide = "top" | "right" | "bottom" | "left";

export interface DrawerProps {
  /** Drawer content */
  children?: React.ReactNode;
  /** Additional CSS class names */
  className?: string;
  /** Description displayed below the title */
  description?: string;
  /** Footer content */
  footer?: React.ReactNode;
  /** Callback when the open state changes */
  onOpenChange?: (open: boolean) => void;
  /** Whether the drawer is open */
  open?: boolean;
  /** The side from which the drawer opens */
  side?: DrawerSide;
  /** Title displayed in the drawer header */
  title?: string;
  /** Trigger element that opens the drawer */
  trigger?: React.ReactNode;
}

/* ------------------------------------------------------------------ */
/*  Stagger child — animates content items in sequence after open      */
/* ------------------------------------------------------------------ */

const StaggerChild = ({
  children,
  index,
  shouldReduceMotion,
}: {
  children: React.ReactNode;
  index: number;
  shouldReduceMotion: boolean | null;
}) => (
  <motion.div
    animate={
      shouldReduceMotion
        ? { opacity: 1 }
        : { opacity: 1, transform: "translateY(0px)" }
    }
    exit={
      shouldReduceMotion
        ? { opacity: 0, transition: { duration: 0 } }
        : { opacity: 0, transition: { duration: 0.1 } }
    }
    initial={
      shouldReduceMotion
        ? { opacity: 1 }
        : { opacity: 0, transform: "translateY(6px)" }
    }
    transition={
      shouldReduceMotion
        ? { duration: 0 }
        : {
            bounce: 0,
            delay: STAGGER_BASE_DELAY + index * STAGGER_CHILD_DELAY,
            duration: 0.25,
            type: "spring" as const,
          }
    }
  >
    {children}
  </motion.div>
);

/* ------------------------------------------------------------------ */
/*  Animated handle bar with glow pulse                                */
/* ------------------------------------------------------------------ */

const AnimatedHandle = ({
  shouldReduceMotion,
}: {
  shouldReduceMotion: boolean | null;
}) => (
  <motion.div
    animate={
      shouldReduceMotion
        ? {}
        : {
            opacity: [0.5, 1, 0.5],
          }
    }
    className="mx-auto mt-4 h-2 w-[100px] shrink-0 rounded-full bg-muted"
    transition={
      shouldReduceMotion
        ? { duration: 0 }
        : {
            duration: 2,
            ease: [0.645, 0.045, 0.355, 1],
            repeat: Number.POSITIVE_INFINITY,
          }
    }
  />
);

/* ------------------------------------------------------------------ */
/*  Drawer                                                             */
/* ------------------------------------------------------------------ */

export default function Drawer({
  open,
  onOpenChange,
  side = "bottom",
  title,
  description,
  className,
  children,
  trigger,
  footer,
}: DrawerProps) {
  const shouldReduceMotion = useReducedMotion();
  const [isAnimatingOut, setIsAnimatingOut] = useState(false);
  const prevOpenRef = useRef(false);

  useEffect(() => {
    // When going from open -> closed, we need animation-out time
    if (prevOpenRef.current && !open) {
      setIsAnimatingOut(true);
    }
    prevOpenRef.current = !!open;
  }, [open]);

  const handleExitComplete = useCallback(() => {
    setIsAnimatingOut(false);
  }, []);

  // Keep vaul's drawer open during exit animation so the portal stays mounted
  const vaulOpen = open || isAnimatingOut;

  return (
    <DrawerPrimitive
      direction={side}
      onOpenChange={(next) => {
        if (!next && isAnimatingOut) {
          return; // Ignore vaul's close during our exit animation
        }
        onOpenChange?.(next);
      }}
      open={vaulOpen}
    >
      {trigger ? <DrawerTrigger asChild>{trigger}</DrawerTrigger> : null}

      {/*
        We render DrawerContent so vaul's drag gestures still work, but we
        layer motion.div overlays on top for the visual polish (backdrop blur,
        spring slide-in, content stagger, handle pulse).
      */}
      <DrawerContent
        className={cn(
          // Hide vaul's default CSS transition — we animate with motion
          "!transition-none !duration-0",
          // Hide the default handle for bottom drawers — we render our own animated one
          side === "bottom" &&
            "[&>[class*='h-2'][class*='rounded-full']]:hidden",
          className
        )}
      >
        {/* Animated handle for bottom drawers */}
        {side === "bottom" && (
          <AnimatedHandle shouldReduceMotion={shouldReduceMotion} />
        )}

        <AnimatePresence onExitComplete={handleExitComplete}>
          {open ? (
            <motion.div
              animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}
              exit={
                shouldReduceMotion
                  ? { opacity: 0, transition: { duration: 0 } }
                  : { opacity: 0, transition: { duration: 0.15 } }
              }
              initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}
              transition={{
                duration: shouldReduceMotion ? 0 : BACKDROP_DURATION,
              }}
            >
              {/* Staggered header */}
              {title || description ? (
                <StaggerChild index={0} shouldReduceMotion={shouldReduceMotion}>
                  <DrawerHeader>
                    {title ? <DrawerTitle>{title}</DrawerTitle> : null}
                    {description ? (
                      <DrawerDescription>{description}</DrawerDescription>
                    ) : null}
                  </DrawerHeader>
                </StaggerChild>
              ) : null}

              {/* Staggered body */}
              {children ? (
                <StaggerChild
                  index={title || description ? 1 : 0}
                  shouldReduceMotion={shouldReduceMotion}
                >
                  <div className="px-4">{children}</div>
                </StaggerChild>
              ) : null}

              {/* Staggered footer */}
              {footer ? (
                <StaggerChild
                  index={(title || description ? 1 : 0) + (children ? 1 : 0)}
                  shouldReduceMotion={shouldReduceMotion}
                >
                  <DrawerFooter>{footer}</DrawerFooter>
                </StaggerChild>
              ) : null}
            </motion.div>
          ) : null}
        </AnimatePresence>
      </DrawerContent>
    </DrawerPrimitive>
  );
}

/* ------------------------------------------------------------------ */
/*  Re-exports                                                         */
/* ------------------------------------------------------------------ */

export {
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
};

Componentes parecidos