If its your first day at Fight Club, you have to fight.
Tyler Durden
The Narrator
Iceland
Japan
Norway
New Zealand
Canada
Installation
Install dependencies
npm i motion clsx tailwind-merge
Add util file
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code
components/ui/draggable-card.tsx
"use client";
import { cn } from "@/lib/utils";
import React, { useRef, useState, useEffect } from "react";
import {
motion,
useMotionValue,
useSpring,
useTransform,
animate,
useVelocity,
useAnimationControls,
} from "motion/react";
export const DraggableCardBody = ({
className,
children,
}: {
className?: string;
children?: React.ReactNode;
}) => {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const cardRef = useRef<HTMLDivElement>(null);
const controls = useAnimationControls();
const [constraints, setConstraints] = useState({
top: 0,
left: 0,
right: 0,
bottom: 0,
});
// physics biatch
const velocityX = useVelocity(mouseX);
const velocityY = useVelocity(mouseY);
const springConfig = {
stiffness: 100,
damping: 20,
mass: 0.5,
};
const rotateX = useSpring(
useTransform(mouseY, [-300, 300], [25, -25]),
springConfig,
);
const rotateY = useSpring(
useTransform(mouseX, [-300, 300], [-25, 25]),
springConfig,
);
const opacity = useSpring(
useTransform(mouseX, [-300, 0, 300], [0.8, 1, 0.8]),
springConfig,
);
const glareOpacity = useSpring(
useTransform(mouseX, [-300, 0, 300], [0.2, 0, 0.2]),
springConfig,
);
useEffect(() => {
// Update constraints when component mounts or window resizes
const updateConstraints = () => {
if (typeof window !== "undefined") {
setConstraints({
top: -window.innerHeight / 2,
left: -window.innerWidth / 2,
right: window.innerWidth / 2,
bottom: window.innerHeight / 2,
});
}
};
updateConstraints();
// Add resize listener
window.addEventListener("resize", updateConstraints);
// Clean up
return () => {
window.removeEventListener("resize", updateConstraints);
};
}, []);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const { clientX, clientY } = e;
const { width, height, left, top } =
cardRef.current?.getBoundingClientRect() ?? {
width: 0,
height: 0,
left: 0,
top: 0,
};
const centerX = left + width / 2;
const centerY = top + height / 2;
const deltaX = clientX - centerX;
const deltaY = clientY - centerY;
mouseX.set(deltaX);
mouseY.set(deltaY);
};
const handleMouseLeave = () => {
mouseX.set(0);
mouseY.set(0);
};
return (
<motion.div
ref={cardRef}
drag
dragConstraints={constraints}
onDragStart={() => {
document.body.style.cursor = "grabbing";
}}
onDragEnd={(event, info) => {
document.body.style.cursor = "default";
controls.start({
rotateX: 0,
rotateY: 0,
transition: {
type: "spring",
...springConfig,
},
});
const currentVelocityX = velocityX.get();
const currentVelocityY = velocityY.get();
const velocityMagnitude = Math.sqrt(
currentVelocityX * currentVelocityX +
currentVelocityY * currentVelocityY,
);
const bounce = Math.min(0.8, velocityMagnitude / 1000);
animate(info.point.x, info.point.x + currentVelocityX * 0.3, {
duration: 0.8,
ease: [0.2, 0, 0, 1],
bounce,
type: "spring",
stiffness: 50,
damping: 15,
mass: 0.8,
});
animate(info.point.y, info.point.y + currentVelocityY * 0.3, {
duration: 0.8,
ease: [0.2, 0, 0, 1],
bounce,
type: "spring",
stiffness: 50,
damping: 15,
mass: 0.8,
});
}}
style={{
rotateX,
rotateY,
opacity,
willChange: "transform",
}}
animate={controls}
whileHover={{ scale: 1.02 }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className={cn(
"relative min-h-96 w-80 overflow-hidden rounded-md bg-neutral-100 p-6 shadow-2xl transform-3d dark:bg-neutral-900",
className,
)}
>
{children}
<motion.div
style={{
opacity: glareOpacity,
}}
className="pointer-events-none absolute inset-0 bg-white select-none"
/>
</motion.div>
);
};
export const DraggableCardContainer = ({
className,
children,
}: {
className?: string;
children?: React.ReactNode;
}) => {
return (
<div className={cn("[perspective:3000px]", className)}>{children}</div>
);
};
Examples
Polaroid collection
If its your first day at Fight Club, you have to fight.
Tyler Durden
The Narrator
Iceland
Japan
Norway
New Zealand
Canada
Grid
How
You
Doin
Single card
Switzerland
Props
DraggableCardBody Props
Prop | Type | Description | Default |
---|---|---|---|
className | string? | Additional CSS classes to apply to the card body | undefined |
children | React.ReactNode? | Content to render inside the card | undefined |
DraggableCardContainer Props
Prop | Type | Description | Default |
---|---|---|---|
className | string? | Additional CSS classes to apply to the container | undefined |
children | React.ReactNode? | Content to render inside the container | undefined |
Build websites faster and 10x better than your competitors with Aceternity UI Pro
With the best in class components and templates, stand out from the crowd and get more attention to your website. Trusted by founders and entrepreneurs from all over the world.
I've been working with Manu for a couple of months now and I can't express enough how impressed I am with his talent. Manu's JavaScript/React web UI programming skills are through the roof. He's he...
Tony Pujals
Founder at Fantastic Realms, Tech Lead at Google