Files
d559acff-e4ee-448d-92d0-d32…/src/components/ui/BorderGlow.tsx
2026-06-16 13:54:10 +00:00

82 lines
3.0 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import { cls } from "@/lib/utils";
import { animate } from "motion/react";
const spread = 40;
const proximity = 64;
const borderWidth = 1.5;
const BorderGlow = ({ className }: { className?: string }) => {
const ref = useRef<HTMLDivElement>(null);
const rafRef = useRef<number>(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
const onPointerMove = (e: PointerEvent) => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() => {
const { left, top, width, height } = el.getBoundingClientRect();
const { clientX: x, clientY: y } = e;
const isActive =
x > left - proximity &&
x < left + width + proximity &&
y > top - proximity &&
y < top + height + proximity;
el.style.setProperty("--active", isActive ? "1" : "0");
if (!isActive) return;
const centerX = left + width / 2;
const centerY = top + height / 2;
const currentAngle = parseFloat(el.style.getPropertyValue("--start")) || 0;
const targetAngle = (Math.atan2(y - centerY, x - centerX) * 180) / Math.PI + 90;
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180;
animate(currentAngle, currentAngle + angleDiff, {
duration: 2,
ease: [0.16, 1, 0.3, 1],
onUpdate: (v) => el.style.setProperty("--start", String(v)),
});
});
};
document.body.addEventListener("pointermove", onPointerMove, { passive: true });
return () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
document.body.removeEventListener("pointermove", onPointerMove);
};
}, []);
const gradient = `radial-gradient(circle, var(--color-accent) 10%, transparent 20%),
radial-gradient(circle at 40% 40%, var(--color-background-accent) 5%, transparent 15%),
repeating-conic-gradient(from 236.84deg at 50% 50%, var(--color-accent) 0%, var(--color-background-accent) 5%, var(--color-accent) 10%)`;
return (
<div
ref={ref}
style={{ "--spread": spread, "--start": 0, "--active": 0, "--border-width": `${borderWidth}px`, "--gradient": gradient } as React.CSSProperties}
className={cls("pointer-events-none absolute inset-0 rounded-[inherit]", className)}
>
<div
className={cls(
"rounded-[inherit]",
'after:content-[""] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--border-width))]',
"after:[border:var(--border-width)_solid_transparent]",
"after:[background:var(--gradient)] after:bg-fixed",
"after:opacity-(--active) after:transition-opacity after:duration-300",
"after:[mask-clip:padding-box,border-box] after:mask-intersect",
"after:mask-[linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]"
)}
/>
</div>
);
};
export default BorderGlow;