All components

Right Motion Drawer

Right Motion Drawer is a React and Tailwind CSS overlay component from UI Layouts, licensed MIT. Copy it and paste it into your project.

UI Layouts MIT overlays

What it is

Right Motion Drawer is a React and Tailwind CSS overlay component from UI Layouts, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i motion lucide-react

Licence

This component comes from UI Layouts and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Right Motion Drawer

./components/ui/motion-drawer.tsx
'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;
./registry/components/drawer/right-motion-drawer.tsx
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 RightMotionDrawerExample() {
  return (
    <div>
      <MotionDrawer
        direction='right'
        buttonOpeningVariants='stay'
        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 RightMotionDrawerExample;

Similar components