SurfSense/surfsense_web/components/ui/spotlight.tsx

80 lines
2.3 KiB
TypeScript
Raw Normal View History

2025-07-27 10:05:37 -07:00
"use client";
import { motion, type SpringOptions, useSpring, useTransform } from "motion/react";
2025-07-27 10:41:15 -07:00
import { useCallback, useEffect, useRef, useState } from "react";
2025-07-27 10:05:37 -07:00
import { cn } from "@/lib/utils";
2025-04-07 23:47:06 -07:00
type SpotlightProps = {
2025-07-27 10:05:37 -07:00
className?: string;
size?: number;
springOptions?: SpringOptions;
2025-04-07 23:47:06 -07:00
};
export function Spotlight({
2025-07-27 10:05:37 -07:00
className,
size = 200,
springOptions = { bounce: 0 },
2025-04-07 23:47:06 -07:00
}: SpotlightProps) {
2025-07-27 10:05:37 -07:00
const containerRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [parentElement, setParentElement] = useState<HTMLElement | null>(null);
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
const mouseX = useSpring(0, springOptions);
const mouseY = useSpring(0, springOptions);
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
const spotlightLeft = useTransform(mouseX, (x) => `${x - size / 2}px`);
const spotlightTop = useTransform(mouseY, (y) => `${y - size / 2}px`);
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
useEffect(() => {
if (containerRef.current) {
const parent = containerRef.current.parentElement;
if (parent) {
parent.style.position = "relative";
parent.style.overflow = "hidden";
setParentElement(parent);
}
}
}, []);
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
const handleMouseMove = useCallback(
(event: MouseEvent) => {
if (!parentElement) return;
const { left, top } = parentElement.getBoundingClientRect();
mouseX.set(event.clientX - left);
mouseY.set(event.clientY - top);
},
[mouseX, mouseY, parentElement]
);
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
useEffect(() => {
if (!parentElement) return;
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
parentElement.addEventListener("mousemove", handleMouseMove);
parentElement.addEventListener("mouseenter", () => setIsHovered(true));
parentElement.addEventListener("mouseleave", () => setIsHovered(false));
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
return () => {
parentElement.removeEventListener("mousemove", handleMouseMove);
parentElement.removeEventListener("mouseenter", () => setIsHovered(true));
parentElement.removeEventListener("mouseleave", () => setIsHovered(false));
};
}, [parentElement, handleMouseMove]);
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
return (
<motion.div
ref={containerRef}
className={cn(
"pointer-events-none absolute rounded-full bg-[radial-gradient(circle_at_center,var(--tw-gradient-stops),transparent_80%)] blur-xl transition-opacity duration-200",
"from-zinc-50 via-zinc-100 to-zinc-200",
isHovered ? "opacity-100" : "opacity-0",
className
)}
style={{
width: size,
height: size,
left: spotlightLeft,
top: spotlightTop,
}}
/>
);
2025-04-07 23:47:06 -07:00
}