Carousel

The Carousel component is an interactive image slider built using React (TypeScript/TSX) and Framer Motion for smooth animations. It displays slides containing an image, title, and description, with animated transitions between slides.

framer-motion
Beach

Beach

Relax by the sea with soft waves, golden sunsets, and a calming breeze that melts stress away.

Installation

Install the required dependencies:

npm install framer-motion
Link to required files
Follow this link and download the required files. - https://github.com/railav61/data/tree/main/forkui-data/carousel
components/ui/carousel.tsx
"use client";
  
  import { motion, AnimatePresence } from "framer-motion";
  import { useState } from "react";
  import { wrap } from "popmotion"; 
  
  type Slide = {
    image: string;
    title: string;
    desc: string;
  };
  
  const slides: Slide[] = [
    {
      image: "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147229/13_n80oya.jpg",
      title: "Beach",
      desc: "Relax by the sea with soft waves, golden sunsets, and a calming breeze that melts stress away.",
    },
    {
      image: "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147234/Mountains_ecz9v9.jpg",
      title: "Mountains",
      desc: "Breathe in fresh mountain air while surrounded by towering peaks, misty mornings, and peaceful silence.",
    },
    {
      image: "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147231/amazon-forest_oxd5lh.jpg",
      title: "Forest",
      desc: "Immerse yourself in lush greenery, gentle sunlight, and the soothing sounds of nature all around.",
    },
    {
      image: "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147231/desert_fuexqn.jpg",
      title: "Desert",
      desc: "Experience the beauty of endless golden sands, dramatic skies, and the quiet power of vast landscapes.",
    },
    {
      image: "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147234/tokyo_o8nb2f.jpg",
      title: "City",
      desc: "Feel the pulse of urban life with glowing skylines, vibrant streets, and endless opportunities.",
    },
  ];
  
  const variants = {
    enter: (direction: number) => ({
      x: direction > 0 ? "100%" : "-100%",
      opacity: 0,
      scale: 1.1, // Slight zoom-in feel
    }),
    center: {
      zIndex: 1,
      x: 0,
      opacity: 1,
      scale: 1,
    },
    exit: (direction: number) => ({
      zIndex: 0,
      x: direction < 0 ? "100%" : "-100%",
      opacity: 0,
      scale: 0.9, // Shrink slightly as it leaves
    }),
  };
  
  export default function Carousel() {
    // Tuple of [page, direction]
    const [[page, direction], setPage] = useState([0, 0]);
  
    // We wrap the index so it stays within the slides array bounds
    const imageIndex = wrap(0, slides.length, page);
  
    const paginate = (newDirection: number) => {
      setPage([page + newDirection, newDirection]);
    };
  
    return (
      <div className="w-full flex justify-center py-10 px-4">
        <div className="relative w-full max-w-4xl h-[500px] overflow-hidden rounded-3xl bg-neutral-900 shadow-2xl">
          <AnimatePresence initial={false} custom={direction} mode="popLayout">
            <motion.div
              key={page}
              custom={direction}
              variants={variants}
              initial="enter"
              animate="center"
              exit="exit"
              transition={{
                x: { type: "spring", stiffness: 300, damping: 80 },
                opacity: { duration: 0.2 },
              }}
              drag="x"
              dragConstraints={{ left: 0, right: 0 }}
              dragElastic={1}
              onDragEnd={(e, { offset, velocity }) => {
                const swipe = Math.abs(offset.x) > 50;
                if (swipe) {
                  paginate(offset.x > 0 ? -1 : 1);
                }
              }}
              className="absolute inset-0 cursor-grab active:cursor-grabbing"
            >
              {/* Background Image */}
              <img
                src={slides[imageIndex].image}
                alt={slides[imageIndex].title}
                className="w-full h-full object-cover select-none"
              />
  
              {/* Content Overlay */}
              <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent flex flex-col justify-end p-8 sm:p-12">
                <motion.h2
                  initial={{ y: 20, opacity: 0 }}
                  animate={{ y: 0, opacity: 1 }}
                  transition={{ delay: 0.2, duration: 0.5 }}
                  className="text-5xl sm:text-7xl font-black text-white uppercase tracking-tighter"
                >
                  {slides[imageIndex].title}
                </motion.h2>
  
                <motion.p
                  initial={{ y: 20, opacity: 0 }}
                  animate={{ y: 0, opacity: 1 }}
                  transition={{ delay: 0.3, duration: 0.5 }}
                  className="text-lg sm:text-2xl font-medium text-white/80 max-w-md italic"
                >
                  {slides[imageIndex].desc}
                </motion.p>
              </div>
            </motion.div>
          </AnimatePresence>
  
          {/* Custom Navigation Buttons */}
          <div className="absolute inset-0 flex items-center justify-between p-4 z-10 pointer-events-none">
            <NavButton onClick={() => paginate(-1)} direction="left" />
            <NavButton onClick={() => paginate(1)} direction="right" />
          </div>
  
          {/* Progress Indicators */}
          <div className="absolute bottom-6 left-1/2 -translate-x-1/2 flex gap-2 z-20">
            {slides.map((_, i) => (
              <div
                key={i}
                className={`h-1.5 rounded-full transition-all duration-300 ${
                  i === imageIndex ? "w-8 bg-white" : "w-2 bg-white/30"
                }`}
              />
            ))}
          </div>
        </div>
      </div>
    );
  }
  
  function NavButton({
    onClick,
    direction,
  }: {
    onClick: () => void;
    direction: "left" | "right";
  }) {
    return (
      <button
        onClick={onClick}
        className="pointer-events-auto h-12 w-12 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 border border-white/20 backdrop-blur-md transition-all group"
      >
        <span
          className={'text-white text-xl transform group-active:scale-90 transition-transform'}
        >
          {direction === "left" ? "←" : "→"}
        </span>
      </button>
    );
  }