UNPKG

@yhattav/react-component-cursor

Version:

A lightweight, TypeScript-first React library for creating beautiful custom cursors with SSR support, smooth animations, and zero dependencies. Perfect for interactive websites, games, and creative applications.

117 lines (95 loc) 3.83 kB
import React, { useEffect, useState, useCallback, useRef } from 'react'; import { NullablePosition } from '../types.js'; export function useMousePosition( id: string, containerRef: React.RefObject<HTMLElement> | undefined, offsetX: number, offsetY: number, throttleMs = 0 ): { position: NullablePosition; setPosition: React.Dispatch<React.SetStateAction<NullablePosition>>; targetPosition: NullablePosition; isVisible: boolean; } { const [position, setPosition] = useState<NullablePosition>({ x: null, y: null }); const [targetPosition, setTargetPosition] = useState<NullablePosition>({ x: null, y: null }); const isInitialized = useRef(false); // Simple rule: visible if we have a valid position const isVisible = targetPosition.x !== null && targetPosition.y !== null; // Core function to check position against container bounds and update target const updateTargetWithBoundsCheck = useCallback((globalPosition: { x: number; y: number }) => { // Apply offsets const adjustedPosition = { x: globalPosition.x + offsetX, y: globalPosition.y + offsetY, }; // Check container bounds if specified if (containerRef?.current) { const rect = containerRef.current.getBoundingClientRect(); const isInside = globalPosition.x >= rect.left && globalPosition.x <= rect.right && globalPosition.y >= rect.top && globalPosition.y <= rect.bottom; if (isInside) { setTargetPosition(adjustedPosition); } else { setTargetPosition({ x: null, y: null }); } } else { setTargetPosition(adjustedPosition); } }, [containerRef, offsetX, offsetY]); // Handle updates from coordinator (mouse movement, scroll, resize) - unified callback const handleUpdate = useCallback((globalPosition: { x: number; y: number }) => { updateTargetWithBoundsCheck(globalPosition); }, [updateTargetWithBoundsCheck]); // Handle mouse leave - hide cursor useEffect(() => { if (!containerRef?.current) return; const container = containerRef.current; const handleMouseLeave = () => { setTargetPosition({ x: null, y: null }); }; container.addEventListener('mouseleave', handleMouseLeave); return () => { container.removeEventListener('mouseleave', handleMouseLeave); }; }, [containerRef]); // Subscribe to CursorCoordinator (dynamically loaded) useEffect(() => { let isCleanedUp = false; // Use an object to store unsubscribe so cleanup can access latest value const subscriptionRef = { unsubscribe: null as (() => void) | null }; // Dynamic import of the entire coordinator chunk import('../utils/CursorCoordinator') .then(({ CursorCoordinator }) => { // Don't subscribe if component already unmounted if (isCleanedUp) return; const cursorCoordinator = CursorCoordinator.getInstance(); subscriptionRef.unsubscribe = cursorCoordinator.subscribe({ id, onPositionChange: handleUpdate, throttleMs, }); }) .catch((error) => { console.warn('Failed to load cursor coordinator:', error); }); return () => { isCleanedUp = true; // Access the latest unsubscribe function via reference subscriptionRef.unsubscribe?.(); }; }, [id, throttleMs, handleUpdate]); // Initialize position when targetPosition first becomes available // After initialization, useSmoothAnimation handles all updates useEffect(() => { if (targetPosition.x !== null && targetPosition.y !== null && !isInitialized.current) { setPosition(targetPosition); isInitialized.current = true; } }, [targetPosition]); return { position, setPosition, targetPosition, isVisible }; }