Circular
A circular, animated menu built with Framer Motion and Lucide icons
Installation
Run the following command
It will create a new file Circular.tsx
inside the components/menu/Circular.tsx
directory.
mkdir -p components/menu && touch components/menu/Circular.tsx
Paste the code
Open the newly created file and paste the following code:
"use client";
import React, { useState } from "react";
import { motion, AnimatePresence, delay, type Variants } from "framer-motion";
import {
Menu,
Home,
Film,
Music,
Layers2,
Newspaper,
Settings,
} from "lucide-react";
const CircularMenu = () => {
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => {
setIsOpen((prev) => !prev);
};
const menuContainerVariants = {
open: {
transition: {
staggerChildren: 0.1, // Stagger for opening
},
},
closed: {
transition: {
staggerChildren: 0.1,
staggerDirection: -1, // Reverse stagger for closing
},
},
};
// Variants for individual menu items
const menuItemVariants: Variants = {
hidden: {
x: 0,
y: 0,
opacity: 0,
scale: 0,
},
visible: (index: number) => ({
x: Math.cos((index * (2 * Math.PI)) / 6) * 150,
y: Math.sin((index * (2 * Math.PI)) / 6) * 150,
opacity: 1,
scale: 1,
delay: index * 0.25,
transition: { type: "spring", stiffness: 300, damping: 20 },
}),
exit: {
x: 0,
y: 0,
opacity: 0,
scale: 0,
transition: { type: "spring", stiffness: 300, damping: 20 },
},
};
const menuItems = [
{ name: "Home", icon: <Home size={30} />, rotation: 0 },
{ name: "Movies", icon: <Film size={30} />, rotation: 60 },
{ name: "Music", icon: <Music size={30} />, rotation: 120 },
{ name: "Sports", icon: <Layers2 size={30} />, rotation: 180 },
{ name: "News", icon: <Newspaper size={30} />, rotation: 240 },
{ name: "Settings", icon: <Settings size={30} />, rotation: 300 },
];
return (
<div className="size-full center">
<motion.div
variants={menuContainerVariants}
initial="closed"
animate={isOpen ? "open" : "closed"}
className="relative w-96 h-96 rounded-full center"
>
{/* Hamburger Menu Button */}
<button
className="size-12 p-2 center gap-2 cursor-pointer bg-primary text-primary-foreground rounded-full flex-col"
onClick={toggleMenu}
>
{Array.from({ length: 2 }).map((_, index) => {
const rotateAngle = index % 2 === 0 ? 45 : -45;
const changeY = index % 2 === 0 ? 5.5 : -5.5;
return (
<motion.span
key={index}
animate={{
rotate: isOpen ? rotateAngle : 0,
y: isOpen ? changeY : 0,
}}
className="w-8 !h-[3px] bg-primary-foreground"
/>
);
})}
</button>
{/* Animate Presence handles entering and exiting */}
<AnimatePresence>
{isOpen &&
menuItems.map((item, index) => (
<motion.div
key={index}
className="absolute w-16 h-16 bg-primary text-primary-foreground flex items-center justify-center rounded-full"
custom={index}
variants={menuItemVariants}
initial="hidden"
animate="visible"
exit="exit"
>
{item.icon}
</motion.div>
))}
</AnimatePresence>
</motion.div>
</div>
);
};
export default CircularMenu;
Credits
Built by Bossadi Zenith