"use client"; import { useRef, useEffect, useState, type ReactNode, type CSSProperties, } from "react"; import { gsap } from "gsap"; const DEFAULT_PARTICLE_COUNT = 12; const DEFAULT_SPOTLIGHT_RADIUS = 400; const DEFAULT_GLOW_COLOR = "220, 68, 22"; const MOBILE_BREAKPOINT = 768; /* ---------- types ---------- */ type CardData = { title: string; description: string; label: string; children?: ReactNode; }; type MagicBentoSectionProps = { eyebrow: string; heading: string; description: string; cards: CardData[]; glowColor?: string; spotlightRadius?: number; particleCount?: number; enableStars?: boolean; enableSpotlight?: boolean; enableBorderGlow?: boolean; enableTilt?: boolean; enableMagnetism?: boolean; clickEffect?: boolean; textAutoHide?: boolean; }; /* ---------- helpers ---------- */ function useMobileDetection() { const [isMobile, setIsMobile] = useState(false); useEffect(() => { const check = () => setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT); check(); window.addEventListener("resize", check); return () => window.removeEventListener("resize", check); }, []); return isMobile; } function calculateSpotlightValues(radius: number) { return { proximity: radius * 0.5, fadeDistance: radius * 0.75 }; } function updateCardGlow( card: HTMLElement, mx: number, my: number, glow: number, radius: number, ) { const r = card.getBoundingClientRect(); card.style.setProperty("--glow-x", `${((mx - r.left) / r.width) * 100}%`); card.style.setProperty("--glow-y", `${((my - r.top) / r.height) * 100}%`); card.style.setProperty("--glow-intensity", glow.toString()); card.style.setProperty("--glow-radius", `${radius}px`); } /* ---------- ParticleCard ---------- */ function ParticleCard({ children, className = "", style, disabled, particleCount, glowColor, enableTilt, enableMagnetism, clickEffect, }: { children: ReactNode; className?: string; style?: CSSProperties; disabled: boolean; particleCount: number; glowColor: string; enableTilt: boolean; enableMagnetism: boolean; clickEffect: boolean; }) { const cardRef = useRef(null); useEffect(() => { if (disabled || !cardRef.current) return; const el = cardRef.current; const particles: HTMLDivElement[] = []; const timeouts: ReturnType[] = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any let magnetAnim: any = null; let hovered = false; const spawnParticles = () => { const { width, height } = el.getBoundingClientRect(); for (let i = 0; i < particleCount; i++) { const tid = setTimeout(() => { if (!hovered) return; const p = document.createElement("div"); p.style.cssText = ` position:absolute;width:4px;height:4px;border-radius:50%; background:rgba(${glowColor},0.9); box-shadow:0 0 6px rgba(${glowColor},0.5); pointer-events:none;z-index:100; left:${Math.random() * width}px;top:${Math.random() * height}px; `; el.appendChild(p); particles.push(p); gsap.fromTo(p, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: "back.out(1.7)" }); gsap.to(p, { x: (Math.random() - 0.5) * 100, y: (Math.random() - 0.5) * 100, rotation: Math.random() * 360, duration: 2 + Math.random() * 2, ease: "none", repeat: -1, yoyo: true }); gsap.to(p, { opacity: 0.3, duration: 1.5, ease: "power2.inOut", repeat: -1, yoyo: true }); }, i * 100); timeouts.push(tid); } }; const clearParticles = () => { timeouts.forEach(clearTimeout); timeouts.length = 0; magnetAnim?.kill(); particles.forEach((p) => gsap.to(p, { scale: 0, opacity: 0, duration: 0.3, ease: "back.in(1.7)", onComplete: () => p.remove() }), ); particles.length = 0; }; const onEnter = () => { hovered = true; spawnParticles(); if (enableTilt) { gsap.to(el, { rotateX: 5, rotateY: 5, duration: 0.3, ease: "power2.out", transformPerspective: 1000 }); } }; const onLeave = () => { hovered = false; clearParticles(); if (enableTilt) gsap.to(el, { rotateX: 0, rotateY: 0, duration: 0.3, ease: "power2.out" }); if (enableMagnetism) gsap.to(el, { x: 0, y: 0, duration: 0.3, ease: "power2.out" }); }; const onMove = (e: MouseEvent) => { if (!enableTilt && !enableMagnetism) return; const rect = el.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const cx = rect.width / 2; const cy = rect.height / 2; if (enableTilt) { gsap.to(el, { rotateX: ((y - cy) / cy) * -10, rotateY: ((x - cx) / cx) * 10, duration: 0.1, ease: "power2.out", transformPerspective: 1000 }); } if (enableMagnetism) { magnetAnim = gsap.to(el, { x: (x - cx) * 0.05, y: (y - cy) * 0.05, duration: 0.3, ease: "power2.out" }); } }; const onClick = (e: MouseEvent) => { if (!clickEffect) return; const rect = el.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const maxD = Math.max( Math.hypot(x, y), Math.hypot(x - rect.width, y), Math.hypot(x, y - rect.height), Math.hypot(x - rect.width, y - rect.height), ); const ripple = document.createElement("div"); ripple.style.cssText = ` position:absolute;width:${maxD * 2}px;height:${maxD * 2}px;border-radius:50%; background:radial-gradient(circle,rgba(${glowColor},0.4) 0%,rgba(${glowColor},0.2) 30%,transparent 70%); left:${x - maxD}px;top:${y - maxD}px;pointer-events:none;z-index:1000; `; el.appendChild(ripple); gsap.fromTo(ripple, { scale: 0, opacity: 1 }, { scale: 1, opacity: 0, duration: 0.8, ease: "power2.out", onComplete: () => ripple.remove() }); }; el.addEventListener("mouseenter", onEnter); el.addEventListener("mouseleave", onLeave); el.addEventListener("mousemove", onMove); el.addEventListener("click", onClick); return () => { hovered = false; el.removeEventListener("mouseenter", onEnter); el.removeEventListener("mouseleave", onLeave); el.removeEventListener("mousemove", onMove); el.removeEventListener("click", onClick); clearParticles(); }; }, [disabled, particleCount, glowColor, enableTilt, enableMagnetism, clickEffect]); return (
{children}
); } /* ---------- GlobalSpotlight ---------- */ function GlobalSpotlight({ gridRef, disabled, spotlightRadius, glowColor, }: { gridRef: React.RefObject; disabled: boolean; spotlightRadius: number; glowColor: string; }) { useEffect(() => { if (disabled || !gridRef.current) return; const spotlight = document.createElement("div"); spotlight.style.cssText = ` position:fixed;width:800px;height:800px;border-radius:50%;pointer-events:none; background:radial-gradient(circle, rgba(${glowColor},0.15) 0%,rgba(${glowColor},0.08) 15%, rgba(${glowColor},0.04) 25%,rgba(${glowColor},0.02) 40%, rgba(${glowColor},0.01) 65%,transparent 70%); z-index:200;opacity:0;transform:translate(-50%,-50%); mix-blend-mode:screen;will-change:transform,opacity; `; document.body.appendChild(spotlight); const grid = gridRef.current; const onMove = (e: MouseEvent) => { const rect = grid.getBoundingClientRect(); const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom; const cards = grid.querySelectorAll(".mb-card"); if (!inside) { gsap.to(spotlight, { opacity: 0, duration: 0.3, ease: "power2.out" }); cards.forEach((c) => c.style.setProperty("--glow-intensity", "0")); return; } const { proximity, fadeDistance } = calculateSpotlightValues(spotlightRadius); let minDist = Infinity; cards.forEach((card) => { const cr = card.getBoundingClientRect(); const cx = cr.left + cr.width / 2; const cy = cr.top + cr.height / 2; const dist = Math.max(0, Math.hypot(e.clientX - cx, e.clientY - cy) - Math.max(cr.width, cr.height) / 2); minDist = Math.min(minDist, dist); const intensity = dist <= proximity ? 1 : dist <= fadeDistance ? (fadeDistance - dist) / (fadeDistance - proximity) : 0; updateCardGlow(card, e.clientX, e.clientY, intensity, spotlightRadius); }); gsap.to(spotlight, { left: e.clientX, top: e.clientY, duration: 0.1, ease: "power2.out" }); const opacity = minDist <= proximity ? 0.8 : minDist <= fadeDistance ? ((fadeDistance - minDist) / (fadeDistance - proximity)) * 0.8 : 0; gsap.to(spotlight, { opacity, duration: opacity > 0 ? 0.2 : 0.5, ease: "power2.out" }); }; const onLeave = () => { grid.querySelectorAll(".mb-card").forEach((c) => c.style.setProperty("--glow-intensity", "0")); gsap.to(spotlight, { opacity: 0, duration: 0.3, ease: "power2.out" }); }; document.addEventListener("mousemove", onMove); document.addEventListener("mouseleave", onLeave); return () => { document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseleave", onLeave); spotlight.remove(); }; }, [disabled, gridRef, spotlightRadius, glowColor]); return null; } /* ---------- MagicBentoSection ---------- */ export function MagicBentoSection({ eyebrow, heading, description, cards, glowColor = DEFAULT_GLOW_COLOR, spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS, particleCount = DEFAULT_PARTICLE_COUNT, enableStars = true, enableSpotlight = true, enableBorderGlow = true, enableTilt = true, enableMagnetism = true, clickEffect = true, textAutoHide = true, }: MagicBentoSectionProps) { const gridRef = useRef(null); const isMobile = useMobileDetection(); const disabled = isMobile; return (
{/* Section heading — matches project's SectionHeading pattern */}

{eyebrow}

{heading}

{description}

{enableSpotlight && ( )}
{cards.map((card, index) => { const cls = [ "mb-card", textAutoHide ? "mb-card--autohide" : "", enableBorderGlow ? "mb-card--glow" : "", ] .filter(Boolean) .join(" "); const cardStyle: CSSProperties = { "--glow-color": glowColor, } as CSSProperties; const content = ( <>
{card.label}

{card.title}

{card.description && (

{card.description}

)} {card.children && (
{card.children}
)}
); if (enableStars) { return ( {content} ); } return (
{content}
); })}
); }