Cursor Trail
January 2024
Hover me
import { ShuffleIcon } from "@radix-ui/react-icons";import { AnimatePresence, motion, useAnimationControls } from "framer-motion";import React, { useEffect, useRef, useState } from "react";type Position = {x: number;y: number;};type EmojiProps = {position: Position;onComplete: () => void;children?: string;};const EMOJI_OPTIONS = ["🌈", "🌸", "🌍", "🥶", "🥵", "🦋"];const MIN_DISTANCE_THRESHOLD = 40;const EmojiContainer = ({ position, onComplete, children }: EmojiProps) => {const divControls = useAnimationControls();const { x, y } = position;const xDelta = Math.floor(Math.random() * 100) - 50;const variants = {end: {x: x + xDelta,y: y + 100,opacity: 0,scale: 0,rotate: 0,transition: {duration: 2.5,},},};useEffect(() => {if (!position) return;divControls.start("end").then(() => onComplete());}, []);return (<motion.divclassName="absolute"initial={{scale: 1,opacity: 1,x: position?.x,y: position?.y,rotate: "12deg",}}variants={variants}animate={divControls}><span className="inline-block text-5xl">{children}</span></motion.div>);};export const CursorTrail = () => {const containerRef = useRef<HTMLDivElement>(null);const [index, setIndex] = React.useState(0);const [emojis, setEmojis] = useState<Array<{ id: string; position: Position }>>([]);const [emoji, setEmoji] = useState<string>("✨");const [isHoverTextVisible, setIsHoverTextVisible] = useState(true);const handleHover = () => {setIsHoverTextVisible(false);};const removeEmoji = (id: string) => {setEmojis((prevEmojis) => prevEmojis.filter((emoji) => emoji.id !== id));};const randomEmoji = () => {const otherEmojis = EMOJI_OPTIONS.filter((e) => e !== emoji);const randomIndex = Math.floor(Math.random() * otherEmojis.length);setEmoji(otherEmojis[randomIndex]);};const draw = (e: { clientX: number; clientY: number }) => {if (!containerRef.current) return;const div = containerRef.current;const rect = div.getBoundingClientRect();const position = {x: e.clientX - rect.left,y: e.clientY - rect.top,};const newEmoji = {id: `${index}-${position.x}-${position.y}`,position,};const lastPosition = emojis[index - 1]?.position;if (!lastPosition) {setIndex(1);setEmojis([newEmoji]);return;}const distance = Math.sqrt(Math.pow(position.x - lastPosition.x, 2) +Math.pow(position.y - lastPosition.y, 2));if (distance < MIN_DISTANCE_THRESHOLD) return;setIndex((prev) => prev + 1);setEmojis((prevEmojis) => [...prevEmojis, newEmoji]);};const mouseMove = (e: { clientX: number; clientY: number }) => {if (!containerRef.current) return;draw(e);};return (<divclassName="relative h-[400px] w-full overflow-hidden"onMouseMove={mouseMove}onMouseOver={handleHover}ref={containerRef}><div className="absolute left-4 top-3 z-10 cursor-pointer"><buttonclassName="bg-mauve inline-block h-4 w-4 rounded-sm text-sm active:scale-95"onClick={randomEmoji}><ShuffleIcon className="h-4 w-4 text-mauve-light-12 transition active:scale-95 dark:text-mauve-dark-12" /></button></div>{isHoverTextVisible && (<div className="absolute inset-0 flex items-center justify-center"><span className="paragraph animate-pulse">Hover me</span></div>)}<AnimatePresence>{emojis?.map(({ position, id }, _) => {return (<EmojiContainerkey={id}position={position}onComplete={() => removeEmoji(id)}>{emoji}</EmojiContainer>);})}</AnimatePresence></div>);};