Drag to spin













"use client";
import Image from "next/image";
import {
PanInfo,
motion,
useAnimation,
useMotionValue,
useTransform,
} from "motion/react";
import { useMediaQuery } from "@react-hook/media-query";
const IMGS = [
"https://images.unsplash.com/photo-1718119128153-645258b5b814?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1718121279036-13650f74640b?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1718113361290-35ca503e6092?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1718113360777-8aadd1985ecd?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1717844519228-40f041234a7d?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1717844519137-62f09a0cbcc6?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1718034363286-999f294f8523?q=80&w=1889&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1718046254440-77bb25734514?q=80&w=1752&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1718046254335-d9ff832c9c3c?q=80&w=1885&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1718070360743-d7103c38b266?q=80&w=1885&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1717960432608-b6faf49eaeb3?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1717968368310-1110eae34644?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1718058248054-5e7704c3c8ad?q=80&w=1893&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"https://images.unsplash.com/photo-1716890385566-dee802c56d2d?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
];
export default function ThreeDPhotoCarousel() {
const isScreenSizeSm = useMediaQuery("(max-width: 640px)");
const cylinderWidth = isScreenSizeSm ? 1100 : 1800;
const faceCount = IMGS.length;
const faceWidth = cylinderWidth / faceCount;
const dragFactor = 0.05;
const radius = cylinderWidth / (2 * Math.PI);
const rotation = useMotionValue(0);
const controls = useAnimation();
const handleDrag = (_: any, info: PanInfo) => {
rotation.set(rotation.get() + info.offset.x * dragFactor);
};
const handleDragEnd = (_: any, info: PanInfo) => {
controls.start({
rotateY: rotation.get() + info.velocity.x * dragFactor,
transition: { type: "spring", stiffness: 100, damping: 30, mass: 0.1 },
});
};
const transform = useTransform(rotation, (value) => {
return `rotate3d(0, 1, 0, ${value}deg)`;
});
return (
<>
<div className="relative h-[800px] w-full overflow-hidden">
{/* <div className="pointer-events-none absolute left-0 top-0 z-10 h-full w-12" />
<div className="pointer-events-none absolute right-0 top-0 z-10 h-full w-12" /> */}
<div
className="flex h-full items-center justify-center"
style={{
perspective: "250px",
transformStyle: "preserve-3d",
transform: "rotateX(0deg)",
}}
>
<motion.div
drag="x"
className="relative flex h-full origin-center cursor-grab justify-center active:cursor-grabbing"
style={{
transform: transform,
rotateY: rotation,
width: cylinderWidth,
transformStyle: "preserve-3d",
}}
onDrag={handleDrag}
onDragEnd={handleDragEnd}
animate={controls}
>
{IMGS.map((url, i) => {
return (
<div
key={i}
className="absolute flex h-full origin-center items-center justify-center p-2"
style={{
width: `${faceWidth}px`,
transform: `rotateY(${
i * (360 / faceCount)
}deg) translateZ(${radius}px)`,
}}
>
<Image
src={url}
alt="img"
width={1200}
height={800}
className="pointer-events-none h-12 w-full rounded-xl object-cover md:h-40"
sizes="(max-width: 768px) 100vw, 33vw"
/>
</div>
);
})}
</motion.div>
</div>
</div>
</>
);
}
Installation
$ pnpm dlx shadcn@latest add https://hasanharman.dev/r/three-d-carousel.jsonUsage
import ThreeDPhotoCarousel from "@/components/three-d-carousel";
export default function Example() {
return <ThreeDPhotoCarousel />;
}