Motion-Primitives

Cursor

Install component

npxshadcn@latest add "https://21st.dev/r/motion-primitives/cursor"
"use client";

import { useRef, useState, SVGProps } from "react";
import { Cursor } from "@/components/ui/cursor";
import { AnimatePresence, motion } from "framer-motion";
import { PlusIcon, } from "lucide-react";

const MouseIcon = (props: SVGProps<SVGSVGElement>) => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width={26}
      height={31}
      fill="none"
      {...props}
    >
      <g clipPath="url(#a)">
        <path
          fill={"#22c55e"}
          fillRule="evenodd"
          stroke={"#fff"}
          strokeLinecap="square"
          strokeWidth={2}
          d="M21.993 14.425 2.549 2.935l4.444 23.108 4.653-10.002z"
          clipRule="evenodd"
        />
      </g>
      <defs>
        <clipPath id="a">
          <path fill={"#22c55e"} d="M0 0h26v31H0z" />
        </clipPath>
      </defs>
    </svg>
  );
};

export function CursorWithComponent() {
  return (
    <div className="py-12">
      <div className="overflow-hidden rounded-[12px] bg-white p-2 shadow-md dark:bg-black">
        <Cursor
          attachToParent
          variants={{
            initial: { scale: 0.3, opacity: 0 },
            animate: { scale: 1, opacity: 1 },
            exit: { scale: 0.3, opacity: 0 },
          }}
          transition={{
            ease: "easeInOut",
            duration: 0.15,
          }}
          className="left-12 top-4"
        >
          <div>
            <MouseIcon className="h-6 w-6" />
            <div className="ml-4 mt-1 rounded-[4px] bg-green-500 px-2 py-0.5 text-neutral-50">
              The city below
            </div>
          </div>
        </Cursor>
        <img
          src="https://i.pinimg.com/564x/a0/6a/5f/a06a5f814569fcf4a67f3ad89ae1babf.jpg"
          alt="Green herbs"
          className="h-40 w-full max-w-32 rounded-[8px] object-cover"
        />
      </div>
    </div>
  );
}

export function CursorWithSpring() {
  return (
    <div>
      <div className="p-4">
        <Cursor
          attachToParent
          variants={{
            initial: { height: 0, opacity: 0, scale: 0.3 },
            animate: { height: "auto", opacity: 1, scale: 1 },
            exit: { height: 0, opacity: 0, scale: 0.3 },
          }}
          transition={{
            type: "spring",
            duration: 0.3,
            bounce: 0.1,
          }}
          className="overflow-hidden"
          springConfig={{
            bounce: 0.01,
          }}
        >
          <img
            src="https://i.pinimg.com/564x/4c/95/69/4c9569ab2928e5ae400a6a34e7c537a0.jpg"
            alt="Christian Church, Eastern Europe"
            className="h-40 w-40"
          />
        </Cursor>
        Christian Church, Eastern Europe
      </div>
    </div>
  );
}

export function CursorWithImage() {
  const [isHovering, setIsHovering] = useState(false);
  const targetRef = useRef < HTMLDivElement > null;

  const handlePositionChange = (x: number, y: number) => {
    if (targetRef.current) {
      const rect = targetRef.current.getBoundingClientRect();
      const isInside =
        x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
      setIsHovering(isInside);
    }
  };

  return (
    <div className="flex h-[400px] w-full items-center justify-center">
      <Cursor
        attachToParent
        variants={{
          initial: { scale: 0.3, opacity: 0 },
          animate: { scale: 1, opacity: 1 },
          exit: { scale: 0.3, opacity: 0 },
        }}
        springConfig={{
          bounce: 0.001,
        }}
        transition={{
          ease: "easeInOut",
          duration: 0.15,
        }}
        onPositionChange={handlePositionChange}
      >
        <motion.div
          animate={{
            width: isHovering ? 80 : 16,
            height: isHovering ? 32 : 16,
          }}
          className="flex items-center justify-center rounded-[24px] bg-gray-500/40 backdrop-blur-md dark:bg-gray-300/40"
        >
          <AnimatePresence>
            {isHovering ? (
              <motion.div
                initial={{ opacity: 0, scale: 0.6 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={{ opacity: 0, scale: 0.6 }}
                className="inline-flex w-full items-center justify-center"
              >
                <div className="inline-flex items-center text-sm text-white dark:text-black">
                  More <PlusIcon className="ml-1 h-4 w-4" />
                </div>
              </motion.div>
            ) : null}
          </AnimatePresence>
        </motion.div>
      </Cursor>
      <div ref={targetRef}>
        <img
          src="https://i.pinimg.com/564x/75/3c/3f/753c3f1a9f85871ffa7a7a78bcf49f66.jpg"
          alt="Olympic logo Paris 2024"
          className="h-52 w-full max-w-48 rounded-[8px] border border-zinc-100 object-cover"
        />
      </div>
    </div>
  );
}

export { CursorWithComponent, CursorWithSpring, CursorWithImage };