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.

1 lines 15.2 kB
{"version":3,"sources":["../src/utils/MouseTracker.ts","../src/utils/CursorCoordinator.ts"],"sourcesContent":["import { isSSR } from './ssr';\n\n// Types for the mouse tracker\nexport interface MouseSubscription {\n id: string;\n callback: (position: { x: number; y: number }) => void;\n throttleMs?: number;\n}\n\ninterface SubscriberState {\n subscription: MouseSubscription;\n lastCallTime: number;\n timeoutId: NodeJS.Timeout | null;\n}\n\nclass MouseTracker {\n private static instance: MouseTracker | null = null;\n private subscribers = new Map<string, SubscriberState>();\n private currentPosition: { x: number; y: number } | null = null;\n private isListening = false;\n private rafId: number | null = null;\n\n private constructor() {\n this.handleMouseMove = this.handleMouseMove.bind(this);\n }\n\n static getInstance(): MouseTracker {\n if (!MouseTracker.instance) {\n MouseTracker.instance = new MouseTracker();\n }\n return MouseTracker.instance;\n }\n\n subscribe(subscription: MouseSubscription): () => void {\n // Skip subscription during SSR\n if (isSSR()) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {}; // No-op cleanup\n }\n\n const subscriberState: SubscriberState = {\n subscription,\n lastCallTime: 0,\n timeoutId: null,\n };\n\n this.subscribers.set(subscription.id, subscriberState);\n \n // Start listening if this is the first subscriber\n if (!this.isListening) {\n this.startListening();\n }\n\n // Immediately notify new subscriber if we have a current position\n if (this.currentPosition) {\n // Use setTimeout to avoid synchronous callback during subscription\n setTimeout(() => {\n if (this.subscribers.has(subscription.id)) {\n this.callSubscriber(subscriberState);\n }\n }, 0);\n }\n\n // Return unsubscribe function\n return () => {\n this.unsubscribe(subscription.id);\n };\n }\n\n private unsubscribe(id: string): void {\n const subscriberState = this.subscribers.get(id);\n if (subscriberState?.timeoutId) {\n clearTimeout(subscriberState.timeoutId);\n }\n \n this.subscribers.delete(id);\n \n // Stop listening if no more subscribers\n if (this.subscribers.size === 0) {\n this.stopListening();\n }\n }\n\n private startListening(): void {\n if (this.isListening || isSSR()) return;\n \n document.addEventListener('mousemove', this.handleMouseMove);\n this.isListening = true;\n \n // Store position globally for persistence across navigation\n this.loadGlobalPosition();\n }\n\n private stopListening(): void {\n if (!this.isListening) return;\n \n document.removeEventListener('mousemove', this.handleMouseMove);\n this.isListening = false;\n \n if (this.rafId) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n\n private handleMouseMove(event: MouseEvent): void {\n const newPosition = { x: event.clientX, y: event.clientY };\n \n this.currentPosition = newPosition;\n this.saveGlobalPosition();\n \n // Use RAF to batch notifications for better performance\n if (this.rafId === null) {\n this.rafId = requestAnimationFrame(() => {\n this.notifySubscribers();\n this.rafId = null;\n });\n }\n }\n\n private notifySubscribers(): void {\n // Don't notify if we don't have a position yet\n if (!this.currentPosition) return;\n \n const currentTime = Date.now();\n \n this.subscribers.forEach((subscriberState) => {\n const { subscription, lastCallTime, timeoutId } = subscriberState;\n const { throttleMs = 0 } = subscription;\n\n // Apply throttling if specified\n if (throttleMs > 0) {\n const timeSinceLastCall = currentTime - lastCallTime;\n \n if (timeSinceLastCall >= throttleMs) {\n // Call immediately\n this.callSubscriber(subscriberState);\n } else {\n // Schedule delayed call\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n \n subscriberState.timeoutId = setTimeout(() => {\n this.callSubscriber(subscriberState);\n subscriberState.timeoutId = null;\n }, throttleMs - timeSinceLastCall);\n }\n } else {\n // No throttling, call immediately\n this.callSubscriber(subscriberState);\n }\n });\n }\n\n private callSubscriber(subscriberState: SubscriberState): void {\n if (!this.currentPosition) return;\n \n subscriberState.subscription.callback(this.currentPosition);\n subscriberState.lastCallTime = Date.now();\n }\n\n\n\n private loadGlobalPosition(): void {\n // Try to load position from global storage for persistence across navigation\n try {\n const stored = (window as any).__mouseTrackerPosition__;\n if (stored && typeof stored.x === 'number' && typeof stored.y === 'number') {\n this.currentPosition = stored;\n }\n } catch {\n // Ignore errors when accessing global position\n }\n }\n\n private saveGlobalPosition(): void {\n // Save current position globally for persistence\n if (this.currentPosition) {\n try {\n (window as any).__mouseTrackerPosition__ = { ...this.currentPosition };\n } catch {\n // Ignore errors when saving global position\n }\n }\n }\n\n // Public method to get current position (useful for initial positioning)\n getCurrentPosition(): { x: number; y: number } | null {\n return this.currentPosition ? { ...this.currentPosition } : null;\n }\n\n // For testing/debugging\n getSubscriberCount(): number {\n return this.subscribers.size;\n }\n\n // Cleanup method for testing\n static resetInstance(): void {\n if (MouseTracker.instance) {\n MouseTracker.instance.stopListening();\n MouseTracker.instance = null;\n }\n }\n}\n\nexport { MouseTracker }; ","import { isSSR } from './ssr';\nimport { MouseTracker } from './MouseTracker';\n\nexport interface CursorSubscription {\n id: string;\n onPositionChange: (position: { x: number; y: number }) => void;\n throttleMs?: number;\n}\n\ninterface CoordinatorSubscriberState {\n subscription: CursorSubscription;\n lastCallTime: number;\n timeoutId: NodeJS.Timeout | null;\n}\n\nclass CursorCoordinator {\n private static instance: CursorCoordinator | null = null;\n private subscribers = new Map<string, CoordinatorSubscriberState>();\n private mouseTracker: MouseTracker;\n private currentGlobalPosition: { x: number; y: number } | null = null;\n private isListening = false;\n private rafId: number | null = null;\n\n private constructor() {\n this.mouseTracker = MouseTracker.getInstance();\n this.handleLayoutChange = this.handleLayoutChange.bind(this);\n }\n\n static getInstance(): CursorCoordinator {\n if (!CursorCoordinator.instance) {\n CursorCoordinator.instance = new CursorCoordinator();\n }\n return CursorCoordinator.instance;\n }\n\n subscribe(subscription: CursorSubscription): () => void {\n if (isSSR()) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const subscriberState: CoordinatorSubscriberState = {\n subscription,\n lastCallTime: 0,\n timeoutId: null,\n };\n\n this.subscribers.set(subscription.id, subscriberState);\n \n // Start listening if this is the first subscriber\n if (!this.isListening) {\n this.startListening();\n }\n\n // Subscribe to mouse tracker for position updates\n const mouseUnsubscribe = this.mouseTracker.subscribe({\n id: `coordinator-${subscription.id}`,\n callback: (position) => {\n this.updateGlobalPosition(position);\n this.notifyPositionChange(subscription.id, position);\n },\n throttleMs: subscription.throttleMs,\n });\n\n // Return combined unsubscribe function\n return () => {\n mouseUnsubscribe();\n this.unsubscribe(subscription.id);\n };\n }\n\n private unsubscribe(id: string): void {\n const subscriberState = this.subscribers.get(id);\n if (subscriberState?.timeoutId) {\n clearTimeout(subscriberState.timeoutId);\n }\n \n this.subscribers.delete(id);\n \n // Stop listening if no more subscribers\n if (this.subscribers.size === 0) {\n this.stopListening();\n }\n }\n\n private startListening(): void {\n if (this.isListening || isSSR()) return;\n \n // Listen for layout-changing events\n document.addEventListener('scroll', this.handleLayoutChange, true);\n window.addEventListener('resize', this.handleLayoutChange);\n this.isListening = true;\n }\n\n private stopListening(): void {\n if (!this.isListening) return;\n \n document.removeEventListener('scroll', this.handleLayoutChange, true);\n window.removeEventListener('resize', this.handleLayoutChange);\n this.isListening = false;\n \n if (this.rafId) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n\n private handleLayoutChange(): void {\n // Batch layout change notifications using RAF\n if (this.rafId === null) {\n this.rafId = requestAnimationFrame(() => {\n this.notifyAllSubscribersWithCurrentPosition();\n this.rafId = null;\n });\n }\n }\n\n private updateGlobalPosition(position: { x: number; y: number }): void {\n this.currentGlobalPosition = position;\n }\n\n private notifyPositionChange(subscriptionId: string, position: { x: number; y: number }): void {\n const subscriberState = this.subscribers.get(subscriptionId);\n if (!subscriberState) return;\n\n subscriberState.subscription.onPositionChange(position);\n }\n\n private notifyAllSubscribersWithCurrentPosition(): void {\n if (!this.currentGlobalPosition) return;\n \n this.subscribers.forEach((subscriberState) => {\n subscriberState.subscription.onPositionChange(this.currentGlobalPosition!);\n });\n }\n\n getSubscriberCount(): number {\n return this.subscribers.size;\n }\n\n // Cleanup method for testing\n static resetInstance(): void {\n if (CursorCoordinator.instance) {\n CursorCoordinator.instance.stopListening();\n CursorCoordinator.instance = null;\n }\n }\n}\n\nexport { CursorCoordinator }; "],"mappings":"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","names":[]}