@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.
527 lines (505 loc) • 19.5 kB
JavaScript
;Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }
var _chunkO4QN3SFNdevjs = require('./chunk-O4QN3SFN.dev.js');
// src/CustomCursor.tsx
var _react = require('react'); var React2 = _interopRequireWildcard(_react);
var _reactdom = require('react-dom');
// src/hooks/useMousePosition.ts
function useMousePosition(id, containerRef, offsetX, offsetY, throttleMs = 0) {
const [position, setPosition] = _react.useState.call(void 0, { x: null, y: null });
const [targetPosition, setTargetPosition] = _react.useState.call(void 0, { x: null, y: null });
const isInitialized = _react.useRef.call(void 0, false);
const isVisible = targetPosition.x !== null && targetPosition.y !== null;
const updateTargetWithBoundsCheck = _react.useCallback.call(void 0, (globalPosition) => {
const adjustedPosition = {
x: globalPosition.x + offsetX,
y: globalPosition.y + offsetY
};
if (containerRef == null ? void 0 : 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]);
const handleUpdate = _react.useCallback.call(void 0, (globalPosition) => {
updateTargetWithBoundsCheck(globalPosition);
}, [updateTargetWithBoundsCheck]);
_react.useEffect.call(void 0, () => {
if (!(containerRef == null ? void 0 : containerRef.current)) return;
const container = containerRef.current;
const handleMouseLeave = () => {
setTargetPosition({ x: null, y: null });
};
container.addEventListener("mouseleave", handleMouseLeave);
return () => {
container.removeEventListener("mouseleave", handleMouseLeave);
};
}, [containerRef]);
_react.useEffect.call(void 0, () => {
let isCleanedUp = false;
const subscriptionRef = { unsubscribe: null };
Promise.resolve().then(() => _interopRequireWildcard(require("./CursorCoordinator-7XSFJ7G2.dev.js"))).then(({ CursorCoordinator }) => {
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 () => {
var _a;
isCleanedUp = true;
(_a = subscriptionRef.unsubscribe) == null ? void 0 : _a.call(subscriptionRef);
};
}, [id, throttleMs, handleUpdate]);
_react.useEffect.call(void 0, () => {
if (targetPosition.x !== null && targetPosition.y !== null && !isInitialized.current) {
setPosition(targetPosition);
isInitialized.current = true;
}
}, [targetPosition]);
return { position, setPosition, targetPosition, isVisible };
}
// src/hooks/useSmoothAnimation.ts
var SMOOTHING_THRESHOLD = 0.1;
function useSmoothAnimation(targetPosition, smoothFactor, setPosition) {
const calculateNewPosition = _react.useCallback.call(void 0,
(currentPosition) => {
if (currentPosition.x === null || currentPosition.y === null || targetPosition.x === null || targetPosition.y === null) {
return currentPosition;
}
const dx = targetPosition.x - currentPosition.x;
const dy = targetPosition.y - currentPosition.y;
if (Math.abs(dx) < SMOOTHING_THRESHOLD && Math.abs(dy) < SMOOTHING_THRESHOLD) {
return currentPosition;
}
return {
x: currentPosition.x + dx / smoothFactor,
y: currentPosition.y + dy / smoothFactor
};
},
[targetPosition.x, targetPosition.y, smoothFactor]
);
const animate = _react.useCallback.call(void 0, () => {
let animationFrameId;
const smoothing = () => {
setPosition((prev) => {
const newPosition = calculateNewPosition(prev);
if (newPosition.x === prev.x && newPosition.y === prev.y) {
return prev;
}
return newPosition;
});
animationFrameId = requestAnimationFrame(smoothing);
};
animationFrameId = requestAnimationFrame(smoothing);
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
}, [calculateNewPosition, setPosition, targetPosition]);
_react.useEffect.call(void 0, () => {
var _a;
if (_chunkO4QN3SFNdevjs.isSSR.call(void 0, )) return;
const mediaQuery = typeof window !== "undefined" && window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
const prefersReducedMotion = (_a = mediaQuery == null ? void 0 : mediaQuery.matches) != null ? _a : false;
if (smoothFactor <= 1 || prefersReducedMotion) {
setPosition(targetPosition);
return;
}
return animate();
}, [smoothFactor, targetPosition.x, targetPosition.y, animate, setPosition]);
}
// src/utils/validation.ts
function validateProps(props) {
if (false) return;
const {
id,
smoothness,
throttleMs,
zIndex,
offset,
containerRef,
centered,
showDevIndicator,
onMove,
onVisibilityChange
} = props;
if (id !== void 0 && typeof id !== "string") {
console.error(
`CustomCursor: 'id' must be a string. Received: ${id} (${typeof id}). Note: empty strings are allowed and will auto-generate a UUID.`
);
}
if (smoothness !== void 0) {
if (typeof smoothness !== "number" || isNaN(smoothness)) {
console.error(
`CustomCursor: 'smoothness' must be a number. Received: ${smoothness} (${typeof smoothness})`
);
} else if (smoothness < 0) {
console.error(
`CustomCursor: 'smoothness' must be non-negative. Received: ${smoothness}. Use 1 for no smoothing, higher values for more smoothing.`
);
} else if (smoothness > 20) {
console.warn(
`CustomCursor: 'smoothness' value ${smoothness} is very high. Values above 20 may cause poor performance. Consider using a lower value.`
);
}
}
if (throttleMs !== void 0) {
if (typeof throttleMs !== "number" || isNaN(throttleMs)) {
console.error(
`CustomCursor: 'throttleMs' must be a number. Received: ${throttleMs} (${typeof throttleMs})`
);
} else if (throttleMs < 0) {
console.error(
`CustomCursor: 'throttleMs' must be non-negative. Received: ${throttleMs}. Use 0 for no throttling.`
);
} else if (throttleMs > 100) {
console.warn(
`CustomCursor: 'throttleMs' value ${throttleMs} is quite high. This may make the cursor feel sluggish. Consider using a lower value (0-50ms).`
);
}
}
if (zIndex !== void 0) {
if (typeof zIndex !== "number" || isNaN(zIndex)) {
console.error(
`CustomCursor: 'zIndex' must be a number. Received: ${zIndex} (${typeof zIndex})`
);
} else if (!Number.isInteger(zIndex)) {
console.warn(
`CustomCursor: 'zIndex' should be an integer. Received: ${zIndex}`
);
}
}
if (offset !== void 0) {
if (typeof offset !== "object" || offset === null) {
console.error(
`CustomCursor: 'offset' must be an object with x and y properties. Received: ${offset}`
);
} else {
const { x, y } = offset;
if (typeof x !== "number" || isNaN(x)) {
console.error(
`CustomCursor: 'offset.x' must be a number. Received: ${x} (${typeof x})`
);
}
if (typeof y !== "number" || isNaN(y)) {
console.error(
`CustomCursor: 'offset.y' must be a number. Received: ${y} (${typeof y})`
);
}
}
}
if (containerRef !== void 0) {
if (typeof containerRef !== "object" || containerRef === null || !("current" in containerRef)) {
console.error(
`CustomCursor: 'containerRef' must be a React ref object (created with useRef). Received: ${containerRef}`
);
}
}
if (centered !== void 0 && typeof centered !== "boolean") {
console.error(
`CustomCursor: 'centered' must be a boolean. Received: ${centered} (${typeof centered})`
);
}
if (showDevIndicator !== void 0 && typeof showDevIndicator !== "boolean") {
console.error(
`CustomCursor: 'showDevIndicator' must be a boolean. Received: ${showDevIndicator} (${typeof showDevIndicator})`
);
}
if (onMove !== void 0 && typeof onMove !== "function") {
console.error(
`CustomCursor: 'onMove' must be a function. Received: ${typeof onMove}`
);
}
if (onVisibilityChange !== void 0 && typeof onVisibilityChange !== "function") {
console.error(
`CustomCursor: 'onVisibilityChange' must be a function. Received: ${typeof onVisibilityChange}`
);
}
}
// src/CustomCursor.tsx
var ANIMATION_DURATION = "0.3s";
var ANIMATION_NAME = "cursorFadeIn";
var DEFAULT_Z_INDEX = 9999;
var DevIndicator = ({ position, show }) => {
var _a, _b;
if (!show) return null;
return /* @__PURE__ */ React2.createElement(
"div",
{
style: {
position: "fixed",
top: 0,
left: 0,
transform: `translate(${(_a = position.x) != null ? _a : 0}px, ${(_b = position.y) != null ? _b : 0}px)`,
width: "50px",
height: "50px",
border: "2px solid red",
borderRadius: "50%",
pointerEvents: "none",
zIndex: 1e4,
opacity: 0.5,
// Center the circle around the cursor
marginLeft: "-25px",
marginTop: "-25px"
}
}
);
};
var arePropsEqual = (prevProps, nextProps) => {
var _a, _b, _c, _d, _e, _f;
if (prevProps.id !== nextProps.id || prevProps.enabled !== nextProps.enabled || prevProps.className !== nextProps.className || prevProps.zIndex !== nextProps.zIndex || prevProps.smoothness !== nextProps.smoothness || prevProps.centered !== nextProps.centered || prevProps.throttleMs !== nextProps.throttleMs || prevProps.showDevIndicator !== nextProps.showDevIndicator) {
return false;
}
if (((_a = prevProps.offset) == null ? void 0 : _a.x) !== ((_b = nextProps.offset) == null ? void 0 : _b.x) || ((_c = prevProps.offset) == null ? void 0 : _c.y) !== ((_d = nextProps.offset) == null ? void 0 : _d.y)) {
return false;
}
if (((_e = prevProps.containerRef) == null ? void 0 : _e.current) !== ((_f = nextProps.containerRef) == null ? void 0 : _f.current)) {
return false;
}
const prevStyle = prevProps.style || {};
const nextStyle = nextProps.style || {};
const prevStyleKeys = Object.keys(prevStyle);
const nextStyleKeys = Object.keys(nextStyle);
if (prevStyleKeys.length !== nextStyleKeys.length) {
return false;
}
for (const key of prevStyleKeys) {
if (prevStyle[key] !== nextStyle[key]) {
return false;
}
}
if (prevProps.onMove !== nextProps.onMove || prevProps.onVisibilityChange !== nextProps.onVisibilityChange) {
return false;
}
if (prevProps.children !== nextProps.children) {
return false;
}
return true;
};
var generateCursorId = () => {
return `cursor-${Math.random().toString(36).substr(2, 9)}-${Date.now().toString(36)}`;
};
var CustomCursor = React2.memo(
({
id,
enabled = true,
children,
className = "",
style = {},
zIndex = DEFAULT_Z_INDEX,
offset = { x: 0, y: 0 },
smoothness = 1,
containerRef,
centered = true,
throttleMs = 0,
showDevIndicator = true,
onMove,
onVisibilityChange,
"data-testid": dataTestId,
role,
"aria-label": ariaLabel
}) => {
const cursorId = React2.useMemo(() => id || generateCursorId(), [id]);
validateProps({
id: cursorId,
enabled,
children,
className,
style,
zIndex,
offset,
smoothness,
containerRef,
centered,
throttleMs,
showDevIndicator,
onMove,
onVisibilityChange
});
const offsetValues = React2.useMemo(() => ({
x: typeof offset === "object" ? offset.x : 0,
y: typeof offset === "object" ? offset.y : 0
}), [offset]);
const isMobile = React2.useMemo(() => _chunkO4QN3SFNdevjs.isMobileDevice.call(void 0, ), []);
const mousePositionHook = useMousePosition(cursorId, containerRef, offsetValues.x, offsetValues.y, throttleMs);
const { position, setPosition, targetPosition, isVisible } = mousePositionHook;
useSmoothAnimation(targetPosition, smoothness, setPosition);
const [portalContainer, setPortalContainer] = React2.useState(null);
const getPortalContainerMemo = React2.useCallback(() => {
const doc = _chunkO4QN3SFNdevjs.safeDocument.call(void 0, );
if (!doc) return null;
const existingContainer = doc.getElementById("cursor-container");
if (existingContainer) {
existingContainer.style.zIndex = zIndex.toString();
return existingContainer;
}
const container = doc.createElement("div");
container.id = "cursor-container";
container.style.position = "fixed";
container.style.top = "0";
container.style.left = "0";
container.style.pointerEvents = "none";
container.style.zIndex = zIndex.toString();
doc.body.appendChild(container);
return container;
}, [zIndex]);
React2.useEffect(() => {
setPortalContainer(getPortalContainerMemo());
return () => {
const doc = _chunkO4QN3SFNdevjs.safeDocument.call(void 0, );
if (!doc) return;
const container = doc.getElementById("cursor-container");
if (container && container.children.length === 0) {
try {
if (container.parentNode) {
container.parentNode.removeChild(container);
}
} catch (e) {
if (true) {
console.warn("Portal container cleanup failed:", e);
}
}
}
};
}, [getPortalContainerMemo]);
const styleSheetContent = React2.useMemo(() => {
const centerTransform = centered ? " translate(-50%, -50%)" : "";
return `
@keyframes ${ANIMATION_NAME} {
from {
opacity: 0;
transform: translate(var(--cursor-x), var(--cursor-y))${centerTransform} scale(0.8);
}
to {
opacity: 1;
transform: translate(var(--cursor-x), var(--cursor-y))${centerTransform} scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
@keyframes ${ANIMATION_NAME} {
from {
opacity: 0;
transform: translate(var(--cursor-x), var(--cursor-y))${centerTransform} scale(1);
}
to {
opacity: 1;
transform: translate(var(--cursor-x), var(--cursor-y))${centerTransform} scale(1);
}
}
}
`;
}, [centered]);
React2.useEffect(() => {
const doc = _chunkO4QN3SFNdevjs.safeDocument.call(void 0, );
if (!doc) return;
const styleId = `cursor-style-${cursorId}`;
const existingStyle = doc.getElementById(styleId);
if (existingStyle) {
existingStyle.remove();
}
const styleSheet = doc.createElement("style");
styleSheet.id = styleId;
styleSheet.textContent = styleSheetContent;
doc.head.appendChild(styleSheet);
return () => {
const style2 = doc.getElementById(styleId);
if (style2) {
try {
style2.remove();
} catch (e) {
if (true) {
console.warn("Style cleanup failed:", e);
}
}
}
};
}, [cursorId, styleSheetContent]);
const handleMove = React2.useCallback(() => {
if (position.x !== null && position.y !== null && typeof onMove === "function") {
const cursorPosition = { x: position.x, y: position.y };
onMove(cursorPosition);
}
}, [position.x, position.y, onMove]);
React2.useEffect(() => {
handleMove();
}, [handleMove]);
const handleVisibilityChange = React2.useCallback(() => {
if (typeof onVisibilityChange === "function") {
const actuallyVisible = enabled && isVisible;
const reason = !enabled ? "disabled" : "container";
onVisibilityChange(actuallyVisible, reason);
}
}, [enabled, isVisible, onVisibilityChange]);
React2.useEffect(() => {
handleVisibilityChange();
}, [handleVisibilityChange]);
React2.useEffect(() => {
if (isMobile && typeof onVisibilityChange === "function") {
onVisibilityChange(false, "touch");
}
}, [isMobile, onVisibilityChange]);
if (isMobile) {
return null;
}
const cursorStyle = React2.useMemo(
() => {
var _a, _b, _c, _d;
const baseTransform = `translate(${(_a = position.x) != null ? _a : 0}px, ${(_b = position.y) != null ? _b : 0}px)`;
const centerTransform = centered ? " translate(-50%, -50%)" : "";
return _chunkO4QN3SFNdevjs.__spreadValues.call(void 0, {
position: "fixed",
top: 0,
left: 0,
transform: baseTransform + centerTransform,
pointerEvents: "none",
zIndex,
opacity: 1,
visibility: "visible",
animation: `${ANIMATION_NAME} ${ANIMATION_DURATION} ease-out`,
"--cursor-x": `${(_c = position.x) != null ? _c : 0}px`,
"--cursor-y": `${(_d = position.y) != null ? _d : 0}px`
}, style);
},
[position.x, position.y, zIndex, centered, style]
);
const globalStyleContent = React2.useMemo(() => `
#cursor-container {
pointer-events: none !important;
}
`, []);
const shouldRender = !_chunkO4QN3SFNdevjs.isSSR.call(void 0, ) && enabled && isVisible && position.x !== null && position.y !== null && portalContainer;
if (!shouldRender) return null;
return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement("style", { id: `cursor-style-global-${cursorId}` }, globalStyleContent), _reactdom.createPortal.call(void 0,
/* @__PURE__ */ React2.createElement(React2.Fragment, { key: `cursor-${cursorId}` }, /* @__PURE__ */ React2.createElement(
"div",
{
id: `custom-cursor-${cursorId}`,
style: cursorStyle,
className,
"aria-hidden": "true",
"data-testid": dataTestId,
role,
"aria-label": ariaLabel
},
children
), /* @__PURE__ */ React2.createElement(DevIndicator, { position, show: showDevIndicator })),
portalContainer
));
},
arePropsEqual
);
CustomCursor.displayName = "CustomCursor";
var CustomCursor_default = CustomCursor;
exports.CustomCursor = CustomCursor_default; exports.browserOnly = _chunkO4QN3SFNdevjs.browserOnly; exports.isBrowser = _chunkO4QN3SFNdevjs.isBrowser; exports.isSSR = _chunkO4QN3SFNdevjs.isSSR; exports.safeDocument = _chunkO4QN3SFNdevjs.safeDocument; exports.safeWindow = _chunkO4QN3SFNdevjs.safeWindow;
//# sourceMappingURL=index.dev.js.map