Implements COMP-139 (command palette), COMP-140 (accessibility), COMP-141 (scroll animations), COMP-145 (keyboard navigation) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
53 lines
1.2 KiB
TypeScript
53 lines
1.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
|
|
interface UseScrollAnimateOptions {
|
|
threshold?: number;
|
|
rootMargin?: string;
|
|
once?: boolean;
|
|
}
|
|
|
|
export function useScrollAnimate<T extends HTMLElement = HTMLDivElement>({
|
|
threshold = 0.1,
|
|
rootMargin = "0px 0px -60px 0px",
|
|
once = true,
|
|
}: UseScrollAnimateOptions = {}) {
|
|
const ref = useRef<T>(null);
|
|
|
|
useEffect(() => {
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
|
|
const prefersReducedMotion = window.matchMedia(
|
|
"(prefers-reduced-motion: reduce)"
|
|
).matches;
|
|
|
|
if (prefersReducedMotion) {
|
|
el.setAttribute("data-animate", "visible");
|
|
return;
|
|
}
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.setAttribute("data-animate", "visible");
|
|
if (once) {
|
|
observer.unobserve(entry.target);
|
|
}
|
|
} else if (!once) {
|
|
entry.target.setAttribute("data-animate", "hidden");
|
|
}
|
|
});
|
|
},
|
|
{ threshold, rootMargin }
|
|
);
|
|
|
|
observer.observe(el);
|
|
return () => observer.disconnect();
|
|
}, [threshold, rootMargin, once]);
|
|
|
|
return ref;
|
|
}
|