forked from emre/www_projectmycelium_com
- Removed unused React imports from components (now using named imports only) - Removed unused cursor state and onMouseMove handler from text-hover-effect - Removed unused H1 import from HomeBlink
140 lines
3.7 KiB
TypeScript
140 lines
3.7 KiB
TypeScript
"use client";
|
|
import { useRef, useEffect, useState } from "react";
|
|
import { motion, useAnimation } from "motion/react";
|
|
|
|
export const TextHoverEffect = ({
|
|
text,
|
|
duration = 6, // loop duration
|
|
}: {
|
|
text: string;
|
|
duration?: number;
|
|
}) => {
|
|
const svgRef = useRef<SVGSVGElement>(null);
|
|
const controls = useAnimation();
|
|
const [hovered, setHovered] = useState(false);
|
|
|
|
// ✅ Animate mask looping automatically
|
|
useEffect(() => {
|
|
const loop = async () => {
|
|
while (true) {
|
|
await controls.start({
|
|
cx: ["20%", "80%", "50%"],
|
|
cy: ["20%", "80%", "50%"],
|
|
transition: {
|
|
duration,
|
|
ease: "easeInOut",
|
|
repeat: 0,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
loop();
|
|
}, [controls, duration]);
|
|
|
|
return (
|
|
<svg
|
|
ref={svgRef}
|
|
width="100%"
|
|
height="100%"
|
|
viewBox="0 0 300 100"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
onMouseEnter={() => setHovered(true)}
|
|
onMouseLeave={() => setHovered(false)}
|
|
className="select-none"
|
|
>
|
|
<defs>
|
|
{/* ✅ Softer cyan gradient */}
|
|
<linearGradient id="textGradient" gradientUnits="userSpaceOnUse">
|
|
{hovered ? (
|
|
<>
|
|
<stop offset="0%" stopColor="#7df3ff" />
|
|
<stop offset="40%" stopColor="#4adffa" />
|
|
<stop offset="70%" stopColor="#18c5e8" />
|
|
<stop offset="100%" stopColor="#0aaecb" />
|
|
</>
|
|
) : (
|
|
<>
|
|
<stop offset="0%" stopColor="#7df3ff33" />
|
|
<stop offset="100%" stopColor="#0aaecb33" />
|
|
</>
|
|
)}
|
|
</linearGradient>
|
|
|
|
{/* ✅ Mask with autoplay motion */}
|
|
<motion.radialGradient
|
|
id="revealMask"
|
|
gradientUnits="userSpaceOnUse"
|
|
r="45%"
|
|
animate={controls}
|
|
initial={{ cx: "50%", cy: "50%" }}
|
|
>
|
|
<stop offset="0%" stopColor="white" />
|
|
<stop offset="100%" stopColor="black" />
|
|
</motion.radialGradient>
|
|
|
|
{/* ✅ Glow */}
|
|
<filter id="glow">
|
|
<feGaussianBlur stdDeviation="3.2" result="blur" />
|
|
<feMerge>
|
|
<feMergeNode in="blur" />
|
|
<feMergeNode in="SourceGraphic" />
|
|
</feMerge>
|
|
</filter>
|
|
|
|
<mask id="textMask">
|
|
<rect x="0" y="0" width="100%" height="100%" fill="url(#revealMask)" />
|
|
</mask>
|
|
</defs>
|
|
|
|
{/* ✅ Background faint stroke */}
|
|
<text
|
|
x="50%"
|
|
y="50%"
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
strokeWidth="0.15"
|
|
className="fill-transparent stroke-neutral-300 dark:stroke-neutral-800 font-[helvetica] text-7xl font-bold"
|
|
style={{ opacity: hovered ? 0.25 : 0.1 }}
|
|
>
|
|
{text}
|
|
</text>
|
|
|
|
{/* ✅ Line drawing animation always plays too */}
|
|
<motion.text
|
|
x="50%"
|
|
y="50%"
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
strokeWidth="0.25"
|
|
className="fill-transparent stroke-cyan-300 font-[helvetica] text-7xl font-bold"
|
|
initial={{ strokeDashoffset: 600, strokeDasharray: 600 }}
|
|
animate={{
|
|
strokeDashoffset: 0,
|
|
strokeDasharray: 600,
|
|
}}
|
|
transition={{
|
|
duration: 2.2,
|
|
ease: "easeInOut",
|
|
}}
|
|
>
|
|
{text}
|
|
</motion.text>
|
|
|
|
{/* ✅ Final filled glowing cyan text (mask reveals it) */}
|
|
<text
|
|
x="50%"
|
|
y="50%"
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
stroke="url(#textGradient)"
|
|
strokeWidth="1.1"
|
|
mask="url(#textMask)"
|
|
filter="url(#glow)"
|
|
className="font-[helvetica] text-7xl font-bold fill-[url(#textGradient)]"
|
|
>
|
|
{text}
|
|
</text>
|
|
</svg>
|
|
);
|
|
};
|