121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState, useCallback, useId } from "react";
|
|
import { motion } from "motion/react";
|
|
|
|
export const TextHoverEffect = ({
|
|
text,
|
|
}: {
|
|
text: string;
|
|
duration?: number;
|
|
}) => {
|
|
const svgRef = useRef<SVGSVGElement>(null);
|
|
const [hovered, setHovered] = useState(false);
|
|
const [cursorPos, setCursorPos] = useState({ x: 150, y: 30 });
|
|
const id = useId();
|
|
const gradientId = `textGradient-${id}`;
|
|
const revealMaskId = `revealMask-${id}`;
|
|
const textMaskId = `textMask-${id}`;
|
|
|
|
const handleMouseMove = useCallback(
|
|
(e: React.MouseEvent<SVGSVGElement>) => {
|
|
if (!svgRef.current) return;
|
|
const rect = svgRef.current.getBoundingClientRect();
|
|
// Map screen coords to viewBox coords (0 0 300 60)
|
|
const x = ((e.clientX - rect.left) / rect.width) * 300;
|
|
const y = ((e.clientY - rect.top) / rect.height) * 60;
|
|
setCursorPos({ x, y });
|
|
},
|
|
[]
|
|
);
|
|
|
|
return (
|
|
<svg
|
|
ref={svgRef}
|
|
width="100%"
|
|
height="100%"
|
|
viewBox="0 0 300 60"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
onMouseEnter={() => setHovered(true)}
|
|
onMouseLeave={() => setHovered(false)}
|
|
onMouseMove={handleMouseMove}
|
|
className="select-none"
|
|
>
|
|
<defs>
|
|
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
|
|
<stop offset="0%" stopColor="#eab308" />
|
|
<stop offset="25%" stopColor="#ef4444" />
|
|
<stop offset="50%" stopColor="#3b82f6" />
|
|
<stop offset="75%" stopColor="#06b6d4" />
|
|
<stop offset="100%" stopColor="#8b5cf6" />
|
|
</linearGradient>
|
|
|
|
<radialGradient
|
|
id={revealMaskId}
|
|
gradientUnits="userSpaceOnUse"
|
|
cx={cursorPos.x}
|
|
cy={cursorPos.y}
|
|
r="80"
|
|
>
|
|
<stop offset="0%" stopColor="white" />
|
|
<stop offset="100%" stopColor="black" />
|
|
</radialGradient>
|
|
|
|
<mask id={textMaskId}>
|
|
<rect
|
|
x="0"
|
|
y="0"
|
|
width="300"
|
|
height="60"
|
|
fill={hovered ? `url(#${revealMaskId})` : "black"}
|
|
/>
|
|
</mask>
|
|
</defs>
|
|
|
|
{/* Faint outline always visible */}
|
|
<text
|
|
x="150"
|
|
y="30"
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
strokeWidth="0.3"
|
|
className="font-display font-bold fill-transparent stroke-neutral-200 dark:stroke-neutral-800"
|
|
style={{ opacity: 0.15, fontSize: "2rem" }}
|
|
>
|
|
{text}
|
|
</text>
|
|
|
|
{/* Animated stroke draw */}
|
|
<motion.text
|
|
x="150"
|
|
y="30"
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
strokeWidth="0.3"
|
|
className="font-display font-bold fill-transparent stroke-neutral-200 dark:stroke-neutral-800"
|
|
style={{ fontSize: "2rem" }}
|
|
initial={{ strokeDashoffset: 1000, strokeDasharray: 1000 }}
|
|
animate={{ strokeDashoffset: 0, strokeDasharray: 1000 }}
|
|
transition={{ duration: 4, ease: "easeInOut" }}
|
|
>
|
|
{text}
|
|
</motion.text>
|
|
|
|
{/* Colored gradient revealed on hover */}
|
|
<text
|
|
x="150"
|
|
y="30"
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
stroke={`url(#${gradientId})`}
|
|
strokeWidth="0.3"
|
|
mask={`url(#${textMaskId})`}
|
|
className="font-display font-bold fill-transparent"
|
|
style={{ fontSize: "2rem" }}
|
|
>
|
|
{text}
|
|
</text>
|
|
</svg>
|
|
);
|
|
};
|