- InteractiveMenu: block body on textRefs callback (void return) - TextHoverEffect: remove stale @ts-expect-error (now unused) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
4.2 KiB
TypeScript
148 lines
4.2 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
export const TextHoverEffect = ({
|
|
text,
|
|
duration,
|
|
className,
|
|
}: {
|
|
text: string;
|
|
duration?: number;
|
|
automatic?: boolean;
|
|
className?: string;
|
|
}) => {
|
|
const svgRef = useRef<SVGSVGElement>(null);
|
|
const [cursor, setCursor] = useState({ x: 0, y: 0 });
|
|
const [hovered, setHovered] = useState(false);
|
|
const [maskPosition, setMaskPosition] = useState({ cx: '50%', cy: '50%' });
|
|
|
|
useEffect(() => {
|
|
if (svgRef.current) {
|
|
const svgRect = svgRef.current.getBoundingClientRect();
|
|
const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100;
|
|
const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100;
|
|
setMaskPosition({ cx: `${cxPercentage}%`, cy: `${cyPercentage}%` });
|
|
}
|
|
}, [cursor]);
|
|
|
|
// First word → blue (#1B9AD6), remaining → emerald (#3CC68A) — matches logo
|
|
const [firstWord, ...rest] = text.split(' ');
|
|
const secondWord = rest.join(' ');
|
|
|
|
return (
|
|
<svg
|
|
ref={svgRef}
|
|
width="100%"
|
|
height="100%"
|
|
viewBox="0 0 920 100"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
onMouseEnter={() => setHovered(true)}
|
|
onMouseLeave={() => setHovered(false)}
|
|
onMouseMove={(e) => setCursor({ x: e.clientX, y: e.clientY })}
|
|
className={cn('cursor-pointer select-none', className)}
|
|
>
|
|
<defs>
|
|
<motion.radialGradient
|
|
id="revealMask"
|
|
gradientUnits="userSpaceOnUse"
|
|
r="22%"
|
|
initial={{ cx: '50%', cy: '50%' }}
|
|
animate={maskPosition}
|
|
transition={{ duration: duration ?? 0, ease: 'easeOut' }}
|
|
>
|
|
<stop offset="0%" stopColor="white" />
|
|
<stop offset="100%" stopColor="black" />
|
|
</motion.radialGradient>
|
|
|
|
<mask id="textMask">
|
|
<rect x="0" y="0" width="100%" height="100%" fill="url(#revealMask)" />
|
|
</mask>
|
|
</defs>
|
|
|
|
{/* Base outlines — always visible at low opacity, correct brand colors */}
|
|
<text
|
|
x="50%"
|
|
y="52%"
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
fontSize={68}
|
|
fontFamily="'Satoshi', sans-serif"
|
|
fontWeight={700}
|
|
letterSpacing={3}
|
|
fill="transparent"
|
|
strokeWidth={0.5}
|
|
>
|
|
<tspan stroke="#1B9AD6" strokeOpacity={0.3}>
|
|
{firstWord}{' '}
|
|
</tspan>
|
|
<tspan stroke="#3CC68A" strokeOpacity={0.3}>
|
|
{secondWord}
|
|
</tspan>
|
|
</text>
|
|
|
|
{/* Animated draw-on stroke */}
|
|
<motion.text
|
|
x="50%"
|
|
y="52%"
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
fontSize={68}
|
|
fontFamily="'Satoshi', sans-serif"
|
|
fontWeight={700}
|
|
letterSpacing={3}
|
|
fill="transparent"
|
|
strokeWidth={0.6}
|
|
initial={{ strokeDashoffset: 1000, strokeDasharray: 1000 }}
|
|
animate={{ strokeDashoffset: 0, strokeDasharray: 1000 }}
|
|
transition={{ duration: 4, ease: 'easeInOut' }}
|
|
>
|
|
<tspan stroke="#1B9AD6">{firstWord} </tspan>
|
|
<tspan stroke="#3CC68A">{secondWord}</tspan>
|
|
</motion.text>
|
|
|
|
{/* Hover fill reveal under radial cursor mask */}
|
|
<text
|
|
x="50%"
|
|
y="52%"
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
fontSize={68}
|
|
fontFamily="'Satoshi', sans-serif"
|
|
fontWeight={700}
|
|
letterSpacing={3}
|
|
strokeWidth={0.5}
|
|
mask="url(#textMask)"
|
|
>
|
|
<tspan
|
|
fill="#1B9AD6"
|
|
stroke="#1B9AD6"
|
|
fillOpacity={hovered ? 0.3 : 0}
|
|
style={{ transition: 'fill-opacity 0.3s' }}
|
|
>
|
|
{firstWord}{' '}
|
|
</tspan>
|
|
<tspan
|
|
fill="#3CC68A"
|
|
stroke="#3CC68A"
|
|
fillOpacity={hovered ? 0.3 : 0}
|
|
style={{ transition: 'fill-opacity 0.3s' }}
|
|
>
|
|
{secondWord}
|
|
</tspan>
|
|
</text>
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
export const FooterBackgroundGradient = () => (
|
|
<div
|
|
className="absolute inset-0 z-0"
|
|
style={{
|
|
background:
|
|
'radial-gradient(110% 110% at 50% 0%, rgba(22,37,32,0.8) 40%, rgba(27,154,214,0.1) 100%)',
|
|
}}
|
|
/>
|
|
);
|