Created
March 25, 2025 19:02
-
-
Save hpiaia/bfefc8d336a79c6cdbd35e4f1ceb0a9c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use client"; | |
import { animate } from "motion/react"; | |
import { memo, useCallback, useEffect, useRef } from "react"; | |
import { cn } from "#/app/_lib/utils"; | |
interface GlowingEffectProps { | |
blur?: number; | |
inactiveZone?: number; | |
proximity?: number; | |
spread?: number; | |
variant?: "default" | "white"; | |
glow?: boolean; | |
className?: string; | |
disabled?: boolean; | |
movementDuration?: number; | |
borderWidth?: number; | |
} | |
const GlowingEffect = memo( | |
({ | |
blur = 0, | |
inactiveZone = 0.1, | |
proximity = 100, | |
spread = 100, | |
variant = "default", | |
glow = true, | |
className, | |
movementDuration = 2, | |
borderWidth = 3, | |
disabled = false, | |
}: GlowingEffectProps) => { | |
const containerRef = useRef<HTMLDivElement>(null); | |
const lastPosition = useRef({ x: 0, y: 0 }); | |
const animationFrameRef = useRef<number>(0); | |
const handleMove = useCallback( | |
(e?: MouseEvent | { x: number; y: number }) => { | |
if (!containerRef.current) return; | |
if (animationFrameRef.current) { | |
cancelAnimationFrame(animationFrameRef.current); | |
} | |
animationFrameRef.current = requestAnimationFrame(() => { | |
const element = containerRef.current; | |
if (!element) return; | |
const { left, top, width, height } = element.getBoundingClientRect(); | |
const mouseX = e?.x ?? lastPosition.current.x; | |
const mouseY = e?.y ?? lastPosition.current.y; | |
if (e) { | |
lastPosition.current = { x: mouseX, y: mouseY }; | |
} | |
const center = [left + width * 0.5, top + height * 0.5]; | |
const distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1]); | |
const inactiveRadius = 0.5 * Math.min(width, height) * inactiveZone; | |
if (distanceFromCenter < inactiveRadius) { | |
element.style.setProperty("--active", "0"); | |
return; | |
} | |
const isActive = | |
mouseX > left - proximity && | |
mouseX < left + width + proximity && | |
mouseY > top - proximity && | |
mouseY < top + height + proximity; | |
element.style.setProperty("--active", isActive ? "1" : "0"); | |
if (!isActive) return; | |
const currentAngle = Number.parseFloat(element.style.getPropertyValue("--start")) || 0; | |
const targetAngle = (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90; | |
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180; | |
const newAngle = currentAngle + angleDiff; | |
animate(currentAngle, newAngle, { | |
duration: movementDuration, | |
ease: [0.16, 1, 0.3, 1], | |
onUpdate: (value) => { | |
element.style.setProperty("--start", String(value)); | |
}, | |
}); | |
}); | |
}, | |
[inactiveZone, proximity, movementDuration], | |
); | |
useEffect(() => { | |
if (disabled) return; | |
const handleScroll = () => handleMove(); | |
const handlePointerMove = (e: PointerEvent) => handleMove(e); | |
window.addEventListener("scroll", handleScroll, { passive: true }); | |
document.body.addEventListener("pointermove", handlePointerMove, { | |
passive: true, | |
}); | |
return () => { | |
if (animationFrameRef.current) { | |
cancelAnimationFrame(animationFrameRef.current); | |
} | |
window.removeEventListener("scroll", handleScroll); | |
document.body.removeEventListener("pointermove", handlePointerMove); | |
}; | |
}, [handleMove, disabled]); | |
return ( | |
<> | |
<div | |
className={cn( | |
"pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity", | |
glow && "opacity-100", | |
variant === "white" && "border-white", | |
disabled && "!block", | |
)} | |
/> | |
<div | |
ref={containerRef} | |
style={ | |
{ | |
"--blur": `${blur}px`, | |
"--spread": spread, | |
"--start": "0", | |
"--active": "0", | |
"--glowingeffect-border-width": `${borderWidth}px`, | |
"--repeating-conic-gradient-times": "5", | |
"--gradient": | |
variant === "white" | |
? `repeating-conic-gradient( | |
from 236.84deg at 50% 50%, | |
var(--black), | |
var(--black) calc(25% / var(--repeating-conic-gradient-times)) | |
)` | |
: `radial-gradient(circle, #a855f7 10%, #a855f700 20%), | |
radial-gradient(circle at 40% 40%, #8b5cf6 5%, #8b5cf600 15%), | |
radial-gradient(circle at 60% 60%, #6d28d9 10%, #6d28d900 20%), | |
radial-gradient(circle at 40% 60%, #9333ea 10%, #9333ea00 20%), | |
repeating-conic-gradient( | |
from 236.84deg at 50% 50%, | |
#7008e7 0%, | |
#7008e7 calc(100% / var(--repeating-conic-gradient-times)) | |
)`, | |
} as React.CSSProperties | |
} | |
className={cn( | |
"pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity", | |
glow && "opacity-100", | |
blur > 0 && "blur-[var(--blur)] ", | |
className, | |
disabled && "!hidden", | |
)} | |
> | |
<div | |
className={cn( | |
"glow", | |
"rounded-[inherit]", | |
'after:content-[""] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]', | |
"after:[border:var(--glowingeffect-border-width)_solid_transparent]", | |
"after:[background:var(--gradient)] after:[background-attachment:fixed]", | |
"after:opacity-[var(--active)] after:transition-opacity after:duration-300", | |
"after:[mask-clip:padding-box,border-box]", | |
"after:[mask-composite:intersect]", | |
"after:[mask-image:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]", | |
)} | |
/> | |
</div> | |
</> | |
); | |
}, | |
); | |
GlowingEffect.displayName = "GlowingEffect"; | |
export { GlowingEffect }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment