Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

283 lines
9.2 KiB
TypeScript

"use client";
import { memo, useCallback, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
// Simple easing function (ease-out cubic)
const easeOutCubic = (t: number) => 1 - (1 - t) ** 3;
// Simple animation helper to replace motion's animate
const animateValue = (
from: number,
to: number,
duration: number,
onUpdate: (value: number) => void,
): { stop: () => void } => {
const startTime = performance.now();
let animationFrame: number;
const tick = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / (duration * 1000), 1);
const easedProgress = easeOutCubic(progress);
const currentValue = from + (to - from) * easedProgress;
onUpdate(currentValue);
if (progress < 1) {
animationFrame = requestAnimationFrame(tick);
}
};
animationFrame = requestAnimationFrame(tick);
return {
stop: () => {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
},
};
};
interface GlowingEffectProps {
blur?: number;
inactiveZone?: number;
proximity?: number;
spread?: number;
variant?: "default" | "white";
glow?: boolean;
className?: string;
enabled?: boolean;
movementDuration?: number;
borderWidth?: number;
}
const GlowingEffect = memo(
({
blur = 0,
inactiveZone = 0.7,
proximity = 0,
spread = 20,
variant = "default",
glow = false,
className,
movementDuration = 2,
borderWidth = 1,
enabled = false,
}: GlowingEffectProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const lastPosition = useRef({ x: 0, y: 0 });
const animationFrameRef = useRef<number>(0);
const animationControlRef = useRef<{ stop: () => void } | null>(null);
const cachedRectRef = useRef<DOMRect | null>(null);
const reducedMotionRef = useRef(false);
const handleMove = useCallback(
(e?: MouseEvent | { x: number; y: number }) => {
if (!containerRef.current || reducedMotionRef.current) return;
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(() => {
const element = containerRef.current;
if (!element) return;
const { left, top, width, height } =
cachedRectRef.current || element.getBoundingClientRect();
const mouseX =
e && typeof e === "object"
? "clientX" in e && typeof (e as any).clientX === "number"
? (e as any).clientX
: "x" in e && typeof (e as any).x === "number"
? (e as any).x
: lastPosition.current.x
: lastPosition.current.x;
const mouseY =
e && typeof e === "object"
? "clientY" in e && typeof (e as any).clientY === "number"
? (e as any).clientY
: "y" in e && typeof (e as any).y === "number"
? (e as any).y
: lastPosition.current.y
: lastPosition.current.y;
if (e) {
lastPosition.current = { x: mouseX, y: mouseY };
}
const centerX = left + width * 0.5;
const centerY = top + height * 0.5;
const distanceFromCenter = Math.hypot(
mouseX - centerX,
mouseY - centerY,
);
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 =
parseFloat(element.style.getPropertyValue("--start")) || 0;
const targetAngle =
(180 * Math.atan2(mouseY - centerY, mouseX - centerX)) / Math.PI +
90;
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180;
const newAngle = currentAngle + angleDiff;
animationControlRef.current?.stop();
animationControlRef.current = animateValue(
currentAngle,
newAngle,
movementDuration,
(value) => {
element.style.setProperty("--start", String(value));
},
);
});
},
[inactiveZone, proximity, movementDuration],
);
// Setup ResizeObserver and prefers-reduced-motion
useEffect(() => {
if (!enabled || !containerRef.current) return;
// Check for reduced motion preference
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
reducedMotionRef.current = mediaQuery.matches;
const handleReducedMotionChange = (e: MediaQueryListEvent) => {
reducedMotionRef.current = e.matches;
};
mediaQuery.addEventListener("change", handleReducedMotionChange);
// Setup ResizeObserver to cache bounding rect
const resizeObserver = new ResizeObserver(() => {
if (containerRef.current) {
cachedRectRef.current = containerRef.current.getBoundingClientRect();
}
});
resizeObserver.observe(containerRef.current);
return () => {
mediaQuery.removeEventListener("change", handleReducedMotionChange);
resizeObserver.disconnect();
};
}, [enabled]);
useEffect(() => {
if (!enabled) 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);
}
animationControlRef.current?.stop();
window.removeEventListener("scroll", handleScroll);
document.body.removeEventListener("pointermove", handlePointerMove);
};
}, [handleMove, enabled]);
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",
!enabled && "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, #dd7bbb 10%, #dd7bbb00 20%),
radial-gradient(circle at 40% 40%, #d79f1e 5%, #d79f1e00 15%),
radial-gradient(circle at 60% 60%, #5a922c 10%, #5a922c00 20%),
radial-gradient(circle at 40% 60%, #4c7894 10%, #4c789400 20%),
repeating-conic-gradient(
from 236.84deg at 50% 50%,
#dd7bbb 0%,
#d79f1e calc(25% / var(--repeating-conic-gradient-times)),
#5a922c calc(50% / var(--repeating-conic-gradient-times)),
#4c7894 calc(75% / var(--repeating-conic-gradient-times)),
#dd7bbb 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-(--blur)",
className,
!enabled && "hidden!",
)}
>
<div
className={cn(
"glow",
"rounded-[inherit]",
"after:absolute",
"after:inset-[calc(-1*var(--glowingeffect-border-width))]",
"after:rounded-[inherit]",
'after:content-[""]',
"after:[border:var(--glowingeffect-border-width)_solid_transparent]",
"after:bg-fixed",
"after:[background:var(--gradient)]",
"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>
</>
);
},
);
GlowingEffect.displayName = "GlowingEffect";
export { GlowingEffect };