Circle Path

PreviousNext

Animated display of objects or images placed at regular intervals along a circular path. You can use this in integration sections, on pages that list your brands, or within bento grids. Built with Tailwind CSS, Framer Motion, and React.

Docs
bunduicomponent

Preview

Loading preview…
examples/motion/animations/circle-path/01/page.tsx
"use client";

import { motion, animate } from "framer-motion";
import { useEffect, useState } from "react";

const images = [
  {
    id: 1,
    url: "https://images.pexels.com/photos/1144176/pexels-photo-1144176.jpeg?auto=compress&cs=tinysrgb&w=400"
  },
  {
    id: 2,
    url: "https://images.pexels.com/photos/1276518/pexels-photo-1276518.jpeg?auto=compress&cs=tinysrgb&w=400"
  },
  {
    id: 3,
    url: "https://images.pexels.com/photos/1591373/pexels-photo-1591373.jpeg?auto=compress&cs=tinysrgb&w=400"
  },
  {
    id: 4,
    url: "https://images.pexels.com/photos/1542252/pexels-photo-1542252.jpeg?auto=compress&cs=tinysrgb&w=400"
  },
  {
    id: 5,
    url: "https://images.pexels.com/photos/1858175/pexels-photo-1858175.jpeg?auto=compress&cs=tinysrgb&w=400"
  },
  {
    id: 6,
    url: "https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg?auto=compress&cs=tinysrgb&w=400"
  },
  {
    id: 7,
    url: "https://images.pexels.com/photos/1640777/pexels-photo-1640777.jpeg?auto=compress&cs=tinysrgb&w=400"
  },
  {
    id: 8,
    url: "https://images.pexels.com/photos/1059078/pexels-photo-1059078.jpeg?auto=compress&cs=tinysrgb&w=400"
  },
  {
    id: 9,
    url: "https://images.pexels.com/photos/1402787/pexels-photo-1402787.jpeg?auto=compress&cs=tinysrgb&w=400"
  }
];

export default function CirclePath() {
  const radius = 42;
  const [rotation, setRotation] = useState(0);
  const [isPaused, setIsPaused] = useState(false);

  useEffect(() => {
    const controls = animate(rotation, -360, {
      duration: 10,
      repeat: Infinity,
      ease: "linear"
    });
    return controls.stop;
  }, [rotation]);

  return (
    <div className="aspect-square w-full max-w-md">
      <div
        className="relative h-full w-full"
        onMouseEnter={() => setIsPaused(true)}
        onMouseLeave={() => setIsPaused(false)}>
        <motion.div
          animate={{ rotate: isPaused ? rotation : -360 }}
          transition={{
            duration: isPaused ? 0 : 60,
            ease: "linear",
            repeat: isPaused ? 0 : Infinity
          }}
          onUpdate={(latest) => {
            if (!isPaused && typeof latest.rotate === "number") {
              setRotation(latest.rotate % 360);
            }
          }}
          className="absolute inset-0">
          {images.map((image, index) => {
            const angle = (index * -360) / images.length;
            const x = radius * Math.cos((angle * Math.PI) / 180);
            const y = radius * Math.sin((angle * Math.PI) / 180);

            return (
              <motion.div
                key={image.id}
                className="absolute size-20"
                style={{
                  left: `calc(50% + ${x}%)`,
                  top: `calc(50% + ${y}%)`,
                  x: "-50%",
                  y: "-50%"
                }}
                initial={{ scale: 0, opacity: 0 }}
                animate={{ scale: 1, opacity: 1 }}
                transition={{
                  delay: index * 0.1,
                  duration: 0.5,
                  type: "spring",
                  stiffness: 260,
                  damping: 20
                }}
                whileHover={{
                  scale: 1.15,
                  zIndex: 10,
                  transition: { duration: 0.2 }
                }}
                whileTap={{ scale: 0.95 }}>
                <motion.div
                  animate={{ rotate: -rotation }}
                  transition={{ duration: 0 }}
                  className="h-full w-full">
                  <img
                    src={image.url}
                    alt={`Image ${image.id}`}
                    className="h-full w-full cursor-pointer rounded-full object-cover"
                    loading="lazy"
                  />
                </motion.div>
              </motion.div>
            );
          })}
        </motion.div>
      </div>
    </div>
  );
}

Installation

npx shadcn@latest add @bundui/circle-path

Usage

import { CirclePath } from "@/components/circle-path"
<CirclePath />