loading
Just a moment.

Swipeable Stack Cards

November 2024

card
card
card
card
card
import { motion, useMotionValue, useTransform, PanInfo } from "framer-motion";
import React, { useState } from "react";

interface CardRotateProps {
  children: React.ReactNode;
  onSendToBack: () => void;
}

function CardRotate({ children, onSendToBack }: CardRotateProps) {
  const x = useMotionValue(0);
  const y = useMotionValue(0);
  const rotateX = useTransform(y, [-100, 100], [60, -60]);
  const rotateY = useTransform(x, [-100, 100], [-60, 60]);

  function handleDragEnd(_: unknown, info: PanInfo) {
    const threshold = 180;
    if (
      Math.abs(info.offset.x) > threshold ||
      Math.abs(info.offset.y) > threshold
    ) {
      onSendToBack();
    } else {
      x.set(0);
      y.set(0);
    }
  }

  return (
    <motion.div
      className="absolute h-52 w-52 cursor-grab"
      style={{ x, y, rotateX, rotateY }}
      drag
      dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }}
      dragElastic={0.6}
      whileTap={{ cursor: "grabbing" }}
      onDragEnd={handleDragEnd}
    >
      {children}
    </motion.div>
  );
}

const SwipeableStackCards = () => {
  const initialCards = [
   {
      id: 1,
      z: 4,
      img: "https://i.pinimg.com/736x/19/85/18/198518ff0d05490feb4ecce0687dd493.jpg",
    },
    {
      id: 2,
      z: 3,
      img: "https://i.pinimg.com/736x/57/53/9d/57539df206199215b2ef81b0f777fb20.jpg",
    },
    {
      id: 3,
      z: 2,
      img: "https://i.pinimg.com/736x/12/a3/20/12a320ca84b1c1b6297cee02a41e627b.jpg",
    },
    {
      id: 4,
      z: 1,
      img: "https://i.pinimg.com/736x/5d/1b/3e/5d1b3e2ea9556a73cac40493655fe647.jpg",
    },
    {
      id: 5,
      z: 0,
      img: "https://i.pinimg.com/736x/b3/27/36/b32736fc354f54d9b725434e3be8e2f4.jpg",
    },
  ];
  const [cards, setCards] = useState(initialCards);

  const sendToBack = (id: number) => {
    setCards((prev) => {
      const newCards = [...prev];
      const index = newCards.findIndex((card) => card.id === id);
      const [card] = newCards.splice(index, 1);
      newCards.unshift(card);
      return newCards;
    });
  };

  return (
    <div className="relative h-52 w-52" style={{ perspective: 600 }}>
      {cards.map((card, index) => {
        return (
          <CardRotate key={card.id} onSendToBack={() => sendToBack(card.id)}>
            <motion.div
              className="h-full w-full rounded-lg"
              animate={{
                rotateZ: (cards.length - index - 1) * 4,
                scale: 1 + index * 0.06 - cards.length * 0.06,
                transformOrigin: "90% 90%",
              }}
              initial={false}
              transition={{ type: "spring", stiffness: 260, damping: 20 }}
            >
              <img
                src={card.img}
                alt="card"
                className="pointer-events-none h-full w-full rounded-lg object-cover"
              />
            </motion.div>
          </CardRotate>
        );
      })}
    </div>
  );
}

export default SwipeableStackCards;