@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.
254 lines (252 loc) • 7.46 kB
JavaScript
import {
__spreadValues,
isSSR
} from "./chunk-YMONBIUG.dev.mjs";
// src/utils/MouseTracker.ts
var _MouseTracker = class _MouseTracker {
constructor() {
this.subscribers = /* @__PURE__ */ new Map();
this.currentPosition = null;
this.isListening = false;
this.rafId = null;
this.handleMouseMove = this.handleMouseMove.bind(this);
}
static getInstance() {
if (!_MouseTracker.instance) {
_MouseTracker.instance = new _MouseTracker();
}
return _MouseTracker.instance;
}
subscribe(subscription) {
if (isSSR()) {
return () => {
};
}
const subscriberState = {
subscription,
lastCallTime: 0,
timeoutId: null
};
this.subscribers.set(subscription.id, subscriberState);
if (!this.isListening) {
this.startListening();
}
if (this.currentPosition) {
setTimeout(() => {
if (this.subscribers.has(subscription.id)) {
this.callSubscriber(subscriberState);
}
}, 0);
}
return () => {
this.unsubscribe(subscription.id);
};
}
unsubscribe(id) {
const subscriberState = this.subscribers.get(id);
if (subscriberState == null ? void 0 : subscriberState.timeoutId) {
clearTimeout(subscriberState.timeoutId);
}
this.subscribers.delete(id);
if (this.subscribers.size === 0) {
this.stopListening();
}
}
startListening() {
if (this.isListening || isSSR()) return;
document.addEventListener("mousemove", this.handleMouseMove);
this.isListening = true;
this.loadGlobalPosition();
}
stopListening() {
if (!this.isListening) return;
document.removeEventListener("mousemove", this.handleMouseMove);
this.isListening = false;
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
}
handleMouseMove(event) {
const newPosition = { x: event.clientX, y: event.clientY };
this.currentPosition = newPosition;
this.saveGlobalPosition();
if (this.rafId === null) {
this.rafId = requestAnimationFrame(() => {
this.notifySubscribers();
this.rafId = null;
});
}
}
notifySubscribers() {
if (!this.currentPosition) return;
const currentTime = Date.now();
this.subscribers.forEach((subscriberState) => {
const { subscription, lastCallTime, timeoutId } = subscriberState;
const { throttleMs = 0 } = subscription;
if (throttleMs > 0) {
const timeSinceLastCall = currentTime - lastCallTime;
if (timeSinceLastCall >= throttleMs) {
this.callSubscriber(subscriberState);
} else {
if (timeoutId) {
clearTimeout(timeoutId);
}
subscriberState.timeoutId = setTimeout(() => {
this.callSubscriber(subscriberState);
subscriberState.timeoutId = null;
}, throttleMs - timeSinceLastCall);
}
} else {
this.callSubscriber(subscriberState);
}
});
}
callSubscriber(subscriberState) {
if (!this.currentPosition) return;
subscriberState.subscription.callback(this.currentPosition);
subscriberState.lastCallTime = Date.now();
}
loadGlobalPosition() {
try {
const stored = window.__mouseTrackerPosition__;
if (stored && typeof stored.x === "number" && typeof stored.y === "number") {
this.currentPosition = stored;
}
} catch (e) {
}
}
saveGlobalPosition() {
if (this.currentPosition) {
try {
window.__mouseTrackerPosition__ = __spreadValues({}, this.currentPosition);
} catch (e) {
}
}
}
// Public method to get current position (useful for initial positioning)
getCurrentPosition() {
return this.currentPosition ? __spreadValues({}, this.currentPosition) : null;
}
// For testing/debugging
getSubscriberCount() {
return this.subscribers.size;
}
// Cleanup method for testing
static resetInstance() {
if (_MouseTracker.instance) {
_MouseTracker.instance.stopListening();
_MouseTracker.instance = null;
}
}
};
_MouseTracker.instance = null;
var MouseTracker = _MouseTracker;
// src/utils/CursorCoordinator.ts
var _CursorCoordinator = class _CursorCoordinator {
constructor() {
this.subscribers = /* @__PURE__ */ new Map();
this.currentGlobalPosition = null;
this.isListening = false;
this.rafId = null;
this.mouseTracker = MouseTracker.getInstance();
this.handleLayoutChange = this.handleLayoutChange.bind(this);
}
static getInstance() {
if (!_CursorCoordinator.instance) {
_CursorCoordinator.instance = new _CursorCoordinator();
}
return _CursorCoordinator.instance;
}
subscribe(subscription) {
if (isSSR()) {
return () => {
};
}
const subscriberState = {
subscription,
lastCallTime: 0,
timeoutId: null
};
this.subscribers.set(subscription.id, subscriberState);
if (!this.isListening) {
this.startListening();
}
const mouseUnsubscribe = this.mouseTracker.subscribe({
id: `coordinator-${subscription.id}`,
callback: (position) => {
this.updateGlobalPosition(position);
this.notifyPositionChange(subscription.id, position);
},
throttleMs: subscription.throttleMs
});
return () => {
mouseUnsubscribe();
this.unsubscribe(subscription.id);
};
}
unsubscribe(id) {
const subscriberState = this.subscribers.get(id);
if (subscriberState == null ? void 0 : subscriberState.timeoutId) {
clearTimeout(subscriberState.timeoutId);
}
this.subscribers.delete(id);
if (this.subscribers.size === 0) {
this.stopListening();
}
}
startListening() {
if (this.isListening || isSSR()) return;
document.addEventListener("scroll", this.handleLayoutChange, true);
window.addEventListener("resize", this.handleLayoutChange);
this.isListening = true;
}
stopListening() {
if (!this.isListening) return;
document.removeEventListener("scroll", this.handleLayoutChange, true);
window.removeEventListener("resize", this.handleLayoutChange);
this.isListening = false;
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
}
handleLayoutChange() {
if (this.rafId === null) {
this.rafId = requestAnimationFrame(() => {
this.notifyAllSubscribersWithCurrentPosition();
this.rafId = null;
});
}
}
updateGlobalPosition(position) {
this.currentGlobalPosition = position;
}
notifyPositionChange(subscriptionId, position) {
const subscriberState = this.subscribers.get(subscriptionId);
if (!subscriberState) return;
subscriberState.subscription.onPositionChange(position);
}
notifyAllSubscribersWithCurrentPosition() {
if (!this.currentGlobalPosition) return;
this.subscribers.forEach((subscriberState) => {
subscriberState.subscription.onPositionChange(this.currentGlobalPosition);
});
}
getSubscriberCount() {
return this.subscribers.size;
}
// Cleanup method for testing
static resetInstance() {
if (_CursorCoordinator.instance) {
_CursorCoordinator.instance.stopListening();
_CursorCoordinator.instance = null;
}
}
};
_CursorCoordinator.instance = null;
var CursorCoordinator = _CursorCoordinator;
export {
CursorCoordinator
};
//# sourceMappingURL=CursorCoordinator-6CTTW3XY.dev.mjs.map