Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useEffect, useRef, useState } from "react"; | |
| /** | |
| * useScrollReveal - Hook to detect when elements come into view | |
| * Adds/removes 'in-view' class for CSS scroll animations | |
| */ | |
| export function useScrollReveal<T extends HTMLElement>( | |
| threshold = 0.1, | |
| rootMargin = "-50px" | |
| ) { | |
| const ref = useRef<T>(null); | |
| const [isInView, setIsInView] = useState(false); | |
| useEffect(() => { | |
| const element = ref.current; | |
| if (!element) return; | |
| const observer = new IntersectionObserver( | |
| ([entry]) => { | |
| if (entry.isIntersecting) { | |
| setIsInView(true); | |
| observer.unobserve(element); | |
| } else { | |
| setIsInView(false); | |
| } | |
| }, | |
| { | |
| threshold, | |
| rootMargin, | |
| } | |
| ); | |
| observer.observe(element); | |
| return () => { | |
| observer.disconnect(); | |
| }; | |
| }, [threshold, rootMargin]); | |
| return { ref, isInView }; | |
| } | |
| /** | |
| * useStaggerReveal - Hook for staggered child animations | |
| */ | |
| export function useStaggerReveal<T extends HTMLElement>(count: number) { | |
| const ref = useRef<T>(null); | |
| const [visibleCount, setVisibleCount] = useState(0); | |
| useEffect(() => { | |
| const element = ref.current; | |
| if (!element) return; | |
| const observer = new IntersectionObserver( | |
| ([entry]) => { | |
| if (entry.isIntersecting) { | |
| // Animate children in with stagger | |
| let current = 0; | |
| const interval = setInterval(() => { | |
| current++; | |
| setVisibleCount(current); | |
| if (current >= count) { | |
| clearInterval(interval); | |
| observer.unobserve(element); | |
| } | |
| }, 60); | |
| return () => clearInterval(interval); | |
| } | |
| }, | |
| { threshold: 0.1, rootMargin: "-50px" } | |
| ); | |
| observer.observe(element); | |
| return () => observer.disconnect(); | |
| }, [count]); | |
| return { ref, visibleCount }; | |
| } | |
| /** | |
| * useParallax - Hook for parallax scroll effects | |
| */ | |
| export function useParallax(speed: number = 0.5) { | |
| const [offset, setOffset] = useState(0); | |
| useEffect(() => { | |
| let ticking = false; | |
| const handleScroll = () => { | |
| if (!ticking) { | |
| requestAnimationFrame(() => { | |
| setOffset(window.scrollY * speed); | |
| ticking = false; | |
| }); | |
| ticking = true; | |
| } | |
| }; | |
| window.addEventListener("scroll", handleScroll, { passive: true }); | |
| return () => { | |
| window.removeEventListener("scroll", handleScroll); | |
| }; | |
| }, [speed]); | |
| return offset; | |
| } | |
| /** | |
| * useReducedMotion - Hook to check user's motion preference | |
| */ | |
| export function useReducedMotion(): boolean { | |
| const [prefersReduced, setPrefersReduced] = useState(() => { | |
| // Check synchronously during render to avoid flash | |
| if (typeof window !== "undefined") { | |
| return window.matchMedia("(prefers-reduced-motion: reduce)").matches; | |
| } | |
| return false; | |
| }); | |
| useEffect(() => { | |
| const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); | |
| const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches); | |
| mediaQuery.addEventListener("change", handler); | |
| return () => mediaQuery.removeEventListener("change", handler); | |
| }, []); | |
| return prefersReduced; | |
| } | |
| /** | |
| * useMousePosition - Hook to track mouse position | |
| */ | |
| export function useMousePosition() { | |
| const [position, setPosition] = useState({ x: 0, y: 0 }); | |
| useEffect(() => { | |
| const handleMouseMove = (e: MouseEvent) => { | |
| setPosition({ x: e.clientX, y: e.clientY }); | |
| }; | |
| window.addEventListener("mousemove", handleMouseMove); | |
| return () => { | |
| window.removeEventListener("mousemove", handleMouseMove); | |
| }; | |
| }, []); | |
| return position; | |
| } | |