Files
Webklar/src/components/ui/text-hover-effect.tsx
2026-05-10 12:19:58 +02:00

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>
);
};