Axil_Accountants/src/components/ui/TextHoverEffect.tsx
Vadym Samoilenko 2c115bab99 fix: resolve all TypeScript build errors
- 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>
2026-02-23 13:55:57 +00:00

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%)',
}}
/>
);