Motion Drawer Default
Motion Drawer Default é 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 é
Motion Drawer Default é 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.
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.
Motion Drawer Default
'use client';
import { cn } from '@/lib/utils';
import { Menu, X } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import type React from 'react';
import { useState } from 'react';
export type SideMenuDirection = 'left' | 'right';
export type ButtonOpeningVariants = 'push' | 'merge' | 'stay';
interface SideMenuProps {
// Appearance
overlayColor?: string;
width?: number;
direction?: SideMenuDirection;
backgroundColor?: string;
// Content
children: React.ReactNode;
// Behavior
isOpen?: boolean;
onToggle?: (isOpen: boolean) => void;
showToggleButton?: boolean;
toggleButtonText?: {
open: string;
close: string;
};
// Styling
btnClassName?: string;
className?: string;
contentClassName?: string;
clsBtnClassName?: string;
overlayClassName?: string;
// Animation
animationConfig?: {
type?: 'spring' | 'tween';
damping?: number;
stiffness?: number;
duration?: number;
};
// Drag behavior
enableDrag?: boolean;
dragThreshold?: number;
// New prop
buttonOpeningVariants?: ButtonOpeningVariants;
}
const getOpenButtonVariants = (
direction: SideMenuDirection,
width: number,
type: ButtonOpeningVariants
) => {
switch (type) {
case 'merge':
return direction === 'left'
? {
closed: { x: 0, opacity: 1, scale: 1, borderRadius: '0.5rem' },
open: {
x: width - 68,
opacity: 0,
scale: 1,
borderRadius: '0rem',
},
}
: {
closed: { x: 0, opacity: 1, scale: 1, borderRadius: '0.5rem' },
open: {
x: 68 - width,
opacity: 0,
scale: 1,
borderRadius: '0rem',
},
};
case 'push':
return direction === 'left'
? { closed: { x: 0, opacity: 1 }, open: { x: width + 20, opacity: 0 } }
: {
closed: { x: 0, opacity: 1 },
open: { x: -(width + 20), opacity: 0 },
};
case 'stay':
default:
return {
closed: { x: 0, opacity: 1 },
open: { x: 0, opacity: 0 },
};
}
};
const MotionDrawer: React.FC<SideMenuProps> = ({
// Appearance
overlayColor = 'rgba(0, 0, 0, 0.3)',
width = 250,
direction = 'left',
backgroundColor = '#ffffff',
// Content
children,
// Behavior
isOpen: controlledIsOpen,
onToggle,
showToggleButton = true,
// Styling
btnClassName = '',
clsBtnClassName = '',
className = '',
contentClassName = '',
overlayClassName = '',
// Animation
animationConfig = {
type: 'spring',
damping: 25,
stiffness: 120,
},
// Drag behavior
enableDrag = true,
dragThreshold = 0.3,
buttonOpeningVariants = 'merge',
}) => {
const [internalIsOpen, setInternalIsOpen] = useState<boolean>(false);
const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
const setIsOpen = (value: boolean) => {
if (controlledIsOpen === undefined) {
setInternalIsOpen(value);
}
onToggle?.(value);
};
const getDrawerVariants = () => {
if (direction === 'left') {
return {
closed: { x: -width },
open: { x: 0 },
};
} else {
return {
closed: { x: width },
open: { x: 0 },
};
}
};
const buttonVariants = getOpenButtonVariants(direction, width, buttonOpeningVariants);
const getDragConstraints = () => {
if (direction === 'left') {
return { left: -width, right: 0 };
} else {
return { left: 0, right: width };
}
};
const handleDragEnd = (_event: any, info: any) => {
if (!enableDrag) return;
const threshold = width * dragThreshold;
const dragDistance = Math.abs(info.offset.x);
if (direction === 'left') {
const isDraggingLeft = info.offset.x < 0;
if (isDraggingLeft && dragDistance > threshold && isOpen) {
setIsOpen(false);
} else if (!isDraggingLeft && dragDistance > threshold && !isOpen) {
setIsOpen(true);
}
} else {
const isDraggingRight = info.offset.x > 0;
if (isDraggingRight && dragDistance > threshold && isOpen) {
setIsOpen(false);
} else if (!isDraggingRight && dragDistance > threshold && !isOpen) {
setIsOpen(true);
}
}
};
const drawerPositionClasses = direction === 'left' ? 'left-0' : 'right-0';
const openButtonPositionClasses = direction === 'left' ? 'top-4 left-4' : 'top-4 right-4';
return (
<>
{showToggleButton && (
<motion.button
className={cn(
`fixed z-99 text-primary cursor-pointer ${openButtonPositionClasses}`,
btnClassName
)}
onClick={() => setIsOpen(true)}
variants={buttonVariants}
animate={isOpen ? 'open' : 'closed'}
transition={animationConfig}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Menu />
{/* Open */}
</motion.button>
)}
<AnimatePresence>
{isOpen && (
<div className={`fixed w-full h-full top-0 left-0 z-9999 ${className}`}>
{/* Overlay */}
<motion.div
className={`absolute w-full h-full top-0 left-0 ${overlayClassName}`}
style={{ backgroundColor: overlayColor }}
onClick={() => setIsOpen(false)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
/>
{/* Drawer */}
<motion.div
className={`absolute h-full shadow-[8px_1px_21px_0px_rgba(17,17,26,0.1)] ${drawerPositionClasses} ${contentClassName}`}
style={{
backgroundColor,
width: `${width}px`,
padding: '60px 30px 30px 30px',
boxSizing: 'border-box',
}}
drag={enableDrag ? 'x' : false}
dragElastic={0.1}
dragConstraints={getDragConstraints()}
dragMomentum={false}
onDragEnd={handleDragEnd}
variants={getDrawerVariants()}
initial='closed'
animate='open'
exit='closed'
transition={animationConfig}
>
{/* Close Button */}
{showToggleButton && (
<motion.button
className={cn(
'absolute top-2 right-8 p-2 text-black cursor-pointer',
clsBtnClassName
)}
onClick={() => setIsOpen(false)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
transition={{ duration: 0.2 }}
>
<X size={20} /> {/* Close */}
</motion.button>
)}
{/* Content */}
<div className='h-full overflow-y-auto'>{children}</div>
</motion.div>
</div>
)}
</AnimatePresence>
</>
);
};
export default MotionDrawer;
import MotionDrawer from '@/components/ui/motion-drawer';
import React from 'react';
interface ListItem {
name: string;
url: string;
}
const data: ListItem[] = [
{ name: '1', url: '/private/loans' },
{ name: '2', url: '/private/cards' },
{ name: '3', url: '/private/deposits' },
{ name: '4', url: '/private/services' },
{ name: '5', url: '/private/services' },
{ name: '6', url: '/private/services' },
{ name: '7', url: '/private/services' },
];
function MotionDrawerExample() {
return (
<div>
<MotionDrawer direction='left' width={300} className='text-black'>
<nav className='space-y-4'>
<h2 className='text-xl font-bold mb-4'>Menu</h2>
<a href='#' className='block p-2 hover:bg-neutral-100 rounded-sm'>
Home
</a>
<a href='#' className='block p-2 hover:bg-neutral-100 rounded-sm'>
About
</a>
<a href='#' className='block p-2 hover:bg-neutral-100 rounded-sm'>
Contact
</a>
</nav>
</MotionDrawer>
<h1 className='text-7xl text-center'>Motion Drawer</h1>
</div>
);
}
export default MotionDrawerExample;