interface ObservedAnimation {
element: HTMLElement | SVGElement;
intersecting: boolean;
}
const animations = new Map();
let observer: IntersectionObserver | null = null;
let reducedMotion: MediaQueryList | null = null;
function updateAnimation(animation: ObservedAnimation) {
const running =
animation.intersecting && document.visibilityState === "visible" && !reducedMotion?.matches;
animation.element.style.setProperty("--visible-animation-state", running ? "running" : "paused");
animation.element.style.setProperty(
"--visible-animation-will-change",
running ? "transform" : "auto",
);
}
function updateAnimations() {
for (const animation of animations.values()) updateAnimation(animation);
}
/** Attach to a stable animation container. All refs share visibility and motion listeners. */
export function observeVisibleAnimation(element: HTMLElement | SVGElement | null) {
if (element === null) return;
element.style.setProperty("--visible-animation-state", "paused");
element.style.setProperty("--visible-animation-will-change", "auto");
if (typeof IntersectionObserver === "undefined") return;
if (observer === null) {
reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
reducedMotion.addEventListener("change", updateAnimations);
document.addEventListener("visibilitychange", updateAnimations);
observer = new IntersectionObserver((entries, source) => {
if (source !== observer) return;
for (const entry of entries) {
const animation = animations.get(entry.target);
if (!animation) continue;
animation.intersecting = entry.isIntersecting;
updateAnimation(animation);
}
});
}
const animation = { element, intersecting: false };
animations.set(element, animation);
observer.observe(element);
return () => {
if (animations.get(element) !== animation) return;
animations.delete(element);
observer?.unobserve(element);
element.style.setProperty("--visible-animation-state", "paused");
element.style.setProperty("--visible-animation-will-change", "auto");
if (animations.size === 0) {
observer?.disconnect();
observer = null;
reducedMotion?.removeEventListener("change", updateAnimations);
reducedMotion = null;
document.removeEventListener("visibilitychange", updateAnimations);
}
};
}