A beautiful interactive gallery component that displays draggable photo cards with smooth animations ✨. Perfect for creating engaging photo galleries 📸, portfolio showcases 🎨, or memory walls 💭 where users can interact with and rearrange images naturally.





npm i clsx tailwind-merge motionlib/utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}GalleryCard.tsx
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import { RefObject, useRef, useState } from "react";
export default function GalleryCard() {
return (
<section className="relative h-[500px] w-[80%] flex items-end justify-end overflow-hidden bg-gray-100">
<h2 className="relative z-0 text-[20px] tracking-tighter text-neutral-600 md:text-[30px] right-20 bottom-0 text-center font-inter font-light">
A gallery of gentle echoes <br></br> Frames full of feelings!
{/* <img src={"/dragCardImgs/sticker.png"} className="w-10 h-10 absolute right-[-28px] bottom-[0px]"></img> */}
</h2>
<Cards></Cards>
</section>
);
};
const Cards = () => {
const containerRef = useRef<HTMLDivElement | null>(null);
return (
<div ref={containerRef} className="absolute inset-0 z-10">
<Card containerRef={containerRef} imgSrc="/dragCardImgs/img1.png" top="0%" left="5%" />
<Card containerRef={containerRef} imgSrc="/dragCardImgs/img4.png" top="0%" left="30%" />
<Card containerRef={containerRef} imgSrc="/dragCardImgs/img5.png" top="35%" left="30%" />
<Card containerRef={containerRef} imgSrc="/dragCardImgs/img6.png" top="35%" left="5%" />
<Card containerRef={containerRef} imgSrc="/dragCardImgs/img8.png" top="15%" left="55%" />
</div>
)
};
interface Props {
imgSrc?: string;
alt?: string;
top?: string;
left?: string;
rotate?: string;
containerRef: RefObject<HTMLDivElement | null>;
className?: string;
}
const Card = ({ containerRef, imgSrc, alt, top, left, rotate, className }: Props) => {
const [zIndex, setZIndex] = useState(0);
const updateZIndex = () => {
const els = document.querySelectorAll(".drag-cards");
let maxZIndex = -Infinity;
els.forEach((el) => {
const zIndex = parseInt(window.getComputedStyle(el).getPropertyValue("z-index"));
if (!isNaN(zIndex) && zIndex > maxZIndex) {
maxZIndex = zIndex
}
});
setZIndex(maxZIndex + 1);
};
return (
<motion.img
initial={{ opacity: 0.1 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 0.3 }}
onMouseDown={updateZIndex}
drag
dragConstraints={containerRef}
dragElastic={0.5}
dragTransition={{
power: 0.2,
timeConstant: 200,
}}
src={imgSrc || "/dragCardImgs/img1.png"} alt={alt} width={1000} height={1000}
style={{ top, left, rotate, zIndex }}
className={cn("drag-cards absolute w-40 h-40 bg-neutral-900 p-3 object-cover cursor-grab hover:cursor-grabbing", className)}>
</motion.img>
)
}