'use client'; import { useEffect, useId, useLayoutEffect, useRef } from 'react'; import styles from './electric-border.module.css'; export interface ElectricBorderProps { /** Border color (hex) */ color?: string; /** Animation speed (1 = normal, higher = faster) */ speed?: number; /** Chaos/turbulence intensity */ chaos?: number; /** Border thickness in pixels */ thickness?: number; /** Child elements to wrap */ children: React.ReactNode; /** Custom className */ className?: string; /** Custom styles */ style?: React.CSSProperties; } function hexToRgba(hex: string, alpha: number = 1): string { if (!hex) return `rgba(0,0,0,${alpha})`; let h = hex.replace('#', ''); if (h.length === 3) { h = h.split('').map((c) => c + c).join(''); } const int = parseInt(h, 16); const r = (int >> 16) & 255; const g = (int >> 8) & 255; const b = int & 255; return `rgba(${r}, ${g}, ${b}, ${alpha})`; } const ElectricBorder: React.FC = ({ children, color = '#5227FF', speed = 1, chaos = 1, thickness = 2, className, style, }) => { const rawId = useId().replace(/[:]/g, ''); const filterId = `turbulent-displace-${rawId}`; const svgRef = useRef(null); const rootRef = useRef(null); const strokeRef = useRef(null); const updateAnim = () => { const svg = svgRef.current; const host = rootRef.current; if (!svg || !host) return; if (strokeRef.current) { (strokeRef.current as any).style.filter = `url(#${filterId})`; } const width = Math.max( 1, Math.round(host.clientWidth || host.getBoundingClientRect().width || 0) ); const height = Math.max( 1, Math.round(host.clientHeight || host.getBoundingClientRect().height || 0) ); const dyAnims = Array.from( svg.querySelectorAll('feOffset > animate[attributeName="dy"]') ) as SVGAnimateElement[]; if (dyAnims.length >= 2) { dyAnims[0].setAttribute('values', `${height}; 0`); dyAnims[1].setAttribute('values', `0; -${height}`); } const dxAnims = Array.from( svg.querySelectorAll('feOffset > animate[attributeName="dx"]') ) as SVGAnimateElement[]; if (dxAnims.length >= 2) { dxAnims[0].setAttribute('values', `${width}; 0`); dxAnims[1].setAttribute('values', `0; -${width}`); } const baseDur = 6; const dur = Math.max(0.001, baseDur / (speed || 1)); [...dyAnims, ...dxAnims].forEach((a) => a.setAttribute('dur', `${dur}s`)); const disp = svg.querySelector('feDisplacementMap'); if (disp) (disp as any).setAttribute('scale', String(30 * (chaos || 1))); const filterEl = svg.querySelector(`#${CSS.escape(filterId)}`); if (filterEl) { filterEl.setAttribute('x', '-200%'); filterEl.setAttribute('y', '-200%'); filterEl.setAttribute('width', '500%'); filterEl.setAttribute('height', '500%'); } requestAnimationFrame(() => { [...dyAnims, ...dxAnims].forEach((a) => { if (typeof (a as any).beginElement === 'function') { try { (a as any).beginElement(); } catch { console.warn('ElectricBorder: beginElement failed'); } } }); }); }; useEffect(() => { updateAnim(); }, [speed, chaos, thickness]); useLayoutEffect(() => { if (!rootRef.current) return; const ro = new ResizeObserver(() => updateAnim()); ro.observe(rootRef.current); updateAnim(); return () => ro.disconnect(); }, []); const borderRadius = style?.borderRadius ?? 'inherit'; const strokeStyle: React.CSSProperties = { borderRadius, borderWidth: thickness, borderStyle: 'solid', borderColor: color, }; const glow1Style: React.CSSProperties = { borderRadius, borderWidth: thickness, borderStyle: 'solid', borderColor: hexToRgba(color, 0.6), filter: `blur(${0.5 + thickness * 0.25}px)`, opacity: 0.5, }; const glow2Style: React.CSSProperties = { borderRadius, borderWidth: thickness, borderStyle: 'solid', borderColor: color, filter: `blur(${2 + thickness * 0.5}px)`, opacity: 0.5, }; const bgGlowStyle: React.CSSProperties = { borderRadius, transform: 'scale(1.08)', filter: 'blur(32px)', opacity: 0.3, zIndex: -1, background: `linear-gradient(-30deg, ${hexToRgba(color, 0.8)}, transparent, ${color})`, }; return (
{children}
); }; export default ElectricBorder;