Menu with Bottom Animation
Architecture with Magnetic Effect
Installation
Copy and paste the following code into your project.
components/ui/hover-glimpse.tsx
"use client";
import React, { useState, useRef } from "react";
import { cn } from "@/lib/utils";
import {
motion,
AnimatePresence,
useSpring,
useMotionValue,
} from "framer-motion";
export interface MenuItem {
title: string;
description?: string;
image: string;
}
interface HoverGlimpseProps {
items: MenuItem[];
className?: string;
imageClassName?: string;
activeItemClassName?: string;
animate?: "top" | "bottom" | "left" | "right" | "fade";
magnetic?: boolean;
}
export function HoverGlimpse({
items,
className,
imageClassName,
activeItemClassName,
animate = "fade",
magnetic = false,
}: HoverGlimpseProps) {
const [activeIndex, setActiveIndex] = useState<number>(0);
const imageRef = useRef<HTMLDivElement>(null);
// Motion values for magnetic effect
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const springConfig = { damping: 15, stiffness: 150 };
const springX = useSpring(mouseX, springConfig);
const springY = useSpring(mouseY, springConfig);
// Handle magnetic effect
const handleMouseMove = (e: React.MouseEvent) => {
if (!magnetic || !imageRef.current) return;
const rect = imageRef.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const deltaX = e.clientX - centerX;
const deltaY = e.clientY - centerY;
mouseX.set(deltaX * 0.1);
mouseY.set(deltaY * 0.1);
};
const handleMouseLeave = () => {
if (!magnetic) return;
mouseX.set(0);
mouseY.set(0);
};
const variants = {
top: {
initial: {
opacity: 0,
y: -50,
scale: 1.1,
filter: "blur(8px)",
},
animate: {
opacity: 1,
y: 0,
scale: 1,
filter: "blur(0px)",
},
exit: {
opacity: 0,
y: 50,
scale: 0.95,
filter: "blur(8px)",
},
},
bottom: {
initial: {
opacity: 0,
y: 50,
scale: 1.1,
filter: "blur(8px)",
},
animate: {
opacity: 1,
y: 0,
scale: 1,
filter: "blur(0px)",
},
exit: {
opacity: 0,
y: -50,
scale: 0.95,
filter: "blur(8px)",
},
},
left: {
initial: {
opacity: 0,
x: -40,
scale: 1.1,
filter: "blur(8px)",
},
animate: {
opacity: 1,
x: 0,
scale: 1,
filter: "blur(0px)",
},
exit: {
opacity: 0,
x: 50,
scale: 0.95,
filter: "blur(8px)",
},
},
right: {
initial: {
opacity: 0,
x: 50,
scale: 1.1,
filter: "blur(8px)",
},
animate: {
opacity: 1,
x: 0,
scale: 1,
filter: "blur(0px)",
},
exit: {
opacity: 0,
x: -50,
scale: 0.95,
filter: "blur(8px)",
},
},
fade: {
initial: {
opacity: 0,
scale: 1.2,
filter: "blur(10px)",
},
animate: {
opacity: 1,
scale: 1,
filter: "blur(0px)",
},
exit: {
opacity: 0,
scale: 0.9,
filter: "blur(10px)",
},
},
};
const transition = {
duration: 0.3,
ease: "easeOut",
};
return (
<div
className={cn("relative flex items-center gap-4", className)}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<div className="flex flex-col space-y-1">
{items.map((item, index) => (
<motion.button
key={index}
className={cn(
"relative px-4 py-2 text-left text-base transition-colors",
"hover:text-black dark:hover:text-white",
activeIndex === index
? "text-black dark:text-white " + activeItemClassName
: "text-neutral-600 dark:text-neutral-400",
)}
onHoverStart={() => setActiveIndex(index)}
>
<span className="relative z-10 block cursor-pointer">
<span className="block text-lg font-medium">{item.title}</span>
{item.description && (
<span className="block text-sm text-neutral-500 dark:text-neutral-400">
{item.description}
</span>
)}
</span>
</motion.button>
))}
</div>
<motion.div
ref={imageRef}
className={cn(
"relative ml-8 aspect-square w-[300px] overflow-hidden rounded-xl",
imageClassName,
)}
style={
magnetic
? {
x: springX,
y: springY,
}
: undefined
}
>
<AnimatePresence mode="wait">
<motion.img
key={activeIndex}
src={items[activeIndex].image}
alt={items[activeIndex].title}
className="absolute inset-0 h-full w-full rounded-xl object-cover"
initial={variants[animate].initial}
animate={variants[animate].animate}
exit={variants[animate].exit}
transition={transition}
/>
</AnimatePresence>
</motion.div>
</div>
);
}
Update the import paths to match your project setup:
import HoverGlimpse from "@/components/ui/hover-glimpse";
Usage
import HoverGlimpse from "@/components/ui/hover-glimpse";
const items = [
{
title: "Modern Design",
description: "Clean lines and minimalist approach",
image: "/images/modern.jpg",
},
{
title: "Natural Beauty",
description: "Organic forms and materials",
image: "/images/nature.jpg",
},
];
export default function Example() {
return <HoverGlimpse items={items} animate="fade" magnetic />;
}
Props
MenuItem Props
Prop | Type | Description |
---|---|---|
title | string | Title of the menu item |
description | string | Description of the menu item |
image | string | Image URL for the menu item |
HoverGlimpse Props
Prop | Type | Default | Description |
---|---|---|---|
items | MenuItem[] | - | Array of menu items to display |
className | string | "" | Optional custom classes for the container |
imageClassName | string | "" | Optional custom classes for the image container |
activeItemClassName | string | "" | Optional custom classes for the active menu item |
animate | "top" | "bottom" | "left" | "right" | "fade" | "fade" | Animation direction for image transitions |
magnetic | boolean | false | Enable magnetic hover effect |
Different Animation Directions
Choose from various animation directions.
<HoverGlimpse items={items} animate="right" />
<HoverGlimpse items={items} animate="left" />
<HoverGlimpse items={items} animate="top" />
<HoverGlimpse items={items} animate="bottom" />
Custom Styling
Add custom classes to style the component.
<HoverGlimpse
items={items}
className="gap-8"
imageClassName="rounded-2xl"
activeItemClassName="font-bold"
/>