Command Palette

Search for a command to run...

Image Reveal

Image reveal with a mouse-sensitive spotlight effect that highlights the border of the div or section as you move your cursor

Six Cap

Portfolio

Room Decor

Portfolio

Porter

Portfolio

LifeStyle

Portfolio
null

Installation

Install the following dependencies:

pnpm add motion

Create a file at components/motion/image-reveal.tsx and paste the given code.

"use client";
 
import { cn } from "@/lib/utils";
import { motion, useSpring } from "motion/react";
import React, {
    useState,
    MouseEvent,
    useRef,
    createContext,
    useContext,
} from "react";
 
interface ImageItem {
    img: string;
    label: string;
}
 
interface ImageRevealContextType {
    setCurrentImage: (item: ImageItem | null) => void;
    containerRef: React.RefObject<HTMLDivElement | null>;
}
 
const ImageRevealContext = createContext<ImageRevealContextType | null>(null);
 
export const ImageReveal = ({
    children,
    className,
}: {
    children: React.ReactNode;
    className?: string;
}) => {
    const [currentImage, setCurrentImage] = useState<ImageItem | null>(null);
    const [opacity, setOpacity] = useState(0);
    const containerRef = useRef<HTMLDivElement>(null);
    const imageRef = useRef<HTMLImageElement>(null);
 
    const springConfig = {
        stiffness: 150,
        damping: 15,
        mass: 0.1,
    };
 
    const imagePosition = {
        x: useSpring(0, springConfig),
        y: useSpring(0, springConfig),
    };
 
    const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
        if (!imageRef.current || !containerRef.current) return;
 
        const containerRect = containerRef.current.getBoundingClientRect();
        const relativeX =
            e.clientX - containerRect.left - imageRef.current.offsetWidth / 2;
        const relativeY =
            e.clientY - containerRect.top - imageRef.current.offsetHeight / 2;
 
        imagePosition.x.set(relativeX);
        imagePosition.y.set(relativeY);
    };
 
    const contextValue = {
        setCurrentImage: (item: ImageItem | null) => {
            setCurrentImage(item);
            setOpacity(item ? 1 : 0);
        },
        containerRef,
    };
 
    return (
        <ImageRevealContext.Provider value={contextValue}>
            <section
                ref={containerRef}
                onMouseMove={handleMouseMove}
                className={cn("relative mx-auto w-4/5 p-4", className)}
            >
                {children}
 
                <motion.img
                    ref={imageRef}
                    src={currentImage?.img || "null"}
                    alt={currentImage?.label || "null"}
                    className="pointer-events-none absolute top-0 left-0 h-[220px] w-[300px] rounded-lg object-cover"
                    style={{
                        x: imagePosition.x,
                        y: imagePosition.y,
                        opacity,
                        transition: "opacity 0.2s ease-in-out",
                    }}
                />
            </section>
        </ImageRevealContext.Provider>
    );
};
 
export const ImageRevealItem = ({
    item,
    children,
    className,
}: {
    item: ImageItem;
    children: React.ReactNode;
    className?: string;
}) => {
    const context = useContext(ImageRevealContext);
 
    if (!context) {
        throw new Error("ImageRevealItem must be used within ImageReveal");
    }
 
    const handleHover = () => context.setCurrentImage(item);
    const handleLeave = () => context.setCurrentImage(null);
 
    return (
        <div
            onMouseEnter={handleHover}
            onMouseMove={handleHover}
            onMouseLeave={handleLeave}
            className={cn(
                "flex w-full cursor-pointer justify-between border-b py-5 text-center last:border-none",
                className,
            )}
        >
            {children}
        </div>
    );
};

Update the import paths to match your project setup.