Logo

Voxlet UI

Magic Wand

An interactive Magic Wand effect that follows the cursor, animating a floating wand and revealing images within tiles as the cursor moves.

Tile image
Tile image
Tile image

Installation

  1. 1

    Install Dependencies

    npm i framer-motion lucide-react
  2. 2

    lib/utils.ts

    import { clsx, type ClassValue } from "clsx"
    import { twMerge } from "tailwind-merge"
        
    export function cn(...inputs: ClassValue[]) {
      return twMerge(clsx(inputs))
    }
  3. 3

    Copy the source code

    @/components/ui/magic-wand.tsx
    "use client"
    
    import { createContext, useContext, useRef, useState, useEffect, type ReactNode } from "react"
    import { motion, useMotionValue, useSpring } from "framer-motion"
    import { Image, type LucideIcon } from "lucide-react"
    import { cn } from "@/lib/utils"
    
    // Context to share mouse position data between container and tiles
    type MagicWandContextType = {
        mousePosition: { x: number; y: number }
    }
    
    const MagicWandContext = createContext<MagicWandContextType | undefined>(undefined)
    
    // Hook to use the magic wand context
    function useMagicWand() {
        const context = useContext(MagicWandContext)
        if (!context) {
            throw new Error("Magic Wand components must be used within a MagicWandContainer")
        }
        return context
    }
    
    // Props for the MagicWandContainer
    interface MagicWandContainerProps {
        children: ReactNode
        className?: string
        wandClassName?: string
    }
    
    export function MagicWandContainer({ children, className, wandClassName }: MagicWandContainerProps) {
        const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
    
        // Motion values for wand position and rotation
        const x = useMotionValue(0)
        const y = useMotionValue(0)
        const rotation = useMotionValue(0)
    
        // Add spring physics for smoother movement
        const springX = useSpring(x, { damping: 25, stiffness: 200 })
        const springY = useSpring(y, { damping: 25, stiffness: 200 })
        const springRotation = useSpring(rotation, { damping: 25, stiffness: 200 })
    
        useEffect(() => {
            const handleMouseMove = (e: MouseEvent) => {
                const { clientX, clientY } = e
                const { innerWidth, innerHeight } = window
    
                // Calculate modified position with multipliers and offsets
                const modifiedX = clientX * 1.3 + innerWidth * -0.15
                const modifiedY = clientY * 0.4 - innerHeight * 0.1
    
                // Update rotation based on mouse position
                const rotationValue = (clientX / innerWidth) * 20 - 10
    
                // Update motion values
                x.set(modifiedX)
                y.set(modifiedY)
                rotation.set(rotationValue)
    
                setMousePosition({ x: modifiedX, y: modifiedY })
            }
    
            window.addEventListener("mousemove", handleMouseMove)
            return () => window.removeEventListener("mousemove", handleMouseMove)
        }, [x, y, rotation])
    
        return (
            <MagicWandContext.Provider value={{ mousePosition }}>
                <div className={cn("flex", className)}>
                    <motion.div
                        className={cn(
                            "w-[10vmin] aspect-[1/10] absolute left-[5%] top-[20%] -translate-x-1/2 z-[100] rounded-[3vmin] shadow-[0vmin_1vmin_4vmin_rgba(0,0,0,0.8)] overflow-hidden pointer-events-none",
                            wandClassName,
                        )}
                        style={{
                            x: springX,
                            y: springY,
                            rotate: springRotation,
                            background: "linear-gradient(to right, rgb(26,24,28) 10%, rgb(42,40,44) 45% 55%, rgb(26,24,28) 90%)",
                        }}
                    >
                        <div
                            className="h-[20%] w-full"
                            style={{
                                background:
                                    "linear-gradient(to right, rgb(212,221,236) 10%, rgb(255,255,255) 45% 55%, rgb(212,221,236) 90%)",
                            }}
                        ></div>
                    </motion.div>
    
                    {children}
                </div>
            </MagicWandContext.Provider>
        )
    }
    
    // Icon configuration type
    interface IconConfig {
        icon?: LucideIcon
        className?: string
    }
    
    // Props for the MagicWandTile
    interface MagicWandTileProps {
        image: string
        icon?: IconConfig | LucideIcon
        className?: string
    }
    
    export function MagicWandTile({ image, icon, className }: MagicWandTileProps) {
        const { mousePosition } = useMagicWand()
        const tileRef = useRef<HTMLDivElement>(null)
        const [tileState, setTileState] = useState({ opacity: 0, blur: 10 })
    
        // Determine the icon component and className
        let IconComponent: LucideIcon = Image // Default to Image
        let iconClassName = "text-[15vmin] text-white/10" // Default className
    
        if (icon) {
            if (typeof icon === "function") {
                // If icon is a direct LucideIcon component
                IconComponent = icon
            } else {
                // If icon is an IconConfig object
                if (icon.icon) {
                    IconComponent = icon.icon
                }
                // Apply custom className if provided
                if (icon.className) {
                    iconClassName = cn(iconClassName, icon.className)
                }
            }
        }
    
        useEffect(() => {
            if (!tileRef.current) return
    
            const updateTileState = () => {
                const rect = tileRef.current?.getBoundingClientRect()
                if (!rect) return
    
                const relativeMouseX = mousePosition.x - rect.left
                const mouseXAsDecimal = Math.max(0, Math.min(relativeMouseX / rect.width, 1))
    
                setTileState({
                    opacity: mouseXAsDecimal,
                    blur: (1 - mouseXAsDecimal) * 10,
                })
            }
    
            updateTileState()
        }, [mousePosition])
    
        return (
            <div
                ref={tileRef}
                className={cn(
                    "tile grid place-items-center w-[38vmin] aspect-square bg-[rgb(31,41,55)] rounded-[6vmin] shadow-[0vmin_3vmin_6vmin_rgba(0,0,0,0.25),inset_0vmin_0.5vmin_1vmin_rgba(255,255,255,0.15)] relative overflow-hidden",
                    className,
                )}
            >
                <IconComponent className={iconClassName} />
                <motion.img
                    src={image}
                    alt="Tile image"
                    className="h-full aspect-square absolute left-0 top-0 object-cover"
                    style={{
                        opacity: tileState.opacity,
                        filter: `blur(${tileState.blur}px)`,
                    }}
                    transition={{ duration: 0.1 }}
                />
            </div>
        )
    }
    
    

Props

MagicWandContainerProps

PropTypeDefaultDescription
childrenReact.ReactNode-Elements that will be wrapped by the magic wand effect.
classNamestringundefinedCustom class names for additional styling of the container.
wandClassNamestringundefinedCustom class names for additional styling of the floating wand.

MagicWandTileProps

PropTypeDefaultDescription
imagestring-The source URL of the image to be revealed.
iconLucideIcon | IconConfigundefinedLucide icon or an object with an icon and optional className.
classNamestringundefinedOptional class names for styling the tile.

IconConfig

PropTypeDefaultDescription
iconLucideIconundefinedLucide icon component.
classNamestringundefinedOptional class names for styling the icon.
Brought to you by Voxlet