draggable-circular-menu
Version:
A customizable React circular menu component that supports dragging.
147 lines (124 loc) • 4.59 kB
text/typescript
import React from "react";
export const useDraggable = ({ initialAngle }: { initialAngle: number }) => {
const [node, setNode] = React.useState<HTMLElement>();
const [angle, setAngle] = React.useState<number>(initialAngle);
const [{ dx, dy }, setOffset] = React.useState({
dx: 0,
dy: 0,
});
const ref = React.useCallback((node: HTMLElement) => {
setNode(node);
}, []);
// Initial position
React.useEffect(() => {
if (!node) {
return;
}
const width = node.getBoundingClientRect().width;
const containerWidth = node.parentElement?.getBoundingClientRect().width;
const radius = containerWidth ? containerWidth / 2 : 0;
const center = radius - width / 2;
const radian = ((angle + 0.25) % 1) * Math.PI * 2 - Math.PI;
const dx = center + radius * Math.cos(radian);
const dy = center + radius * Math.sin(radian);
setOffset({ dx, dy });
}, [node]);
const handleMouseDown = React.useCallback(
(e: MouseEvent) => {
if (!node) {
return;
}
const startPos = {
x: e.clientX - dx,
y: e.clientY - dy,
};
const width = node.getBoundingClientRect().width;
const containerWidth = node.parentElement?.getBoundingClientRect().width;
const radius = containerWidth ? containerWidth / 2 : 0;
const center = radius - width / 2;
const handleMouseMove = (e: MouseEvent) => {
let dx = e.clientX - startPos.x;
let dy = e.clientY - startPos.y;
const centerDistance = Math.sqrt(
Math.pow(dx - center, 2) + Math.pow(dy - center, 2)
);
const sinValue = (dy - center) / centerDistance;
const cosValue = (dx - center) / centerDistance;
dx = center + radius * cosValue;
dy = center + radius * sinValue;
const radians = Math.atan2(dy - center, dx - center);
const angle = (radians + Math.PI) / (Math.PI * 2);
setAngle((angle + 0.75) % 1);
setOffset({ dx, dy });
updateCursor();
};
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
resetCursor();
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[node, dx, dy]
);
const handleTouchStart = React.useCallback(
(e: TouchEvent) => {
if (!node) {
return;
}
const touch = e.touches[0];
const startPos = {
x: touch.clientX - dx,
y: touch.clientY - dy,
};
const width = node.getBoundingClientRect().width;
const containerWidth = node.parentElement?.getBoundingClientRect().width;
const radius = containerWidth ? containerWidth / 2 : 0;
const center = radius - width / 2;
const handleTouchMove = (e: TouchEvent) => {
const touch = e.touches[0];
let dx = touch.clientX - startPos.x;
let dy = touch.clientY - startPos.y;
const centerDistance = Math.sqrt(
Math.pow(dx - center, 2) + Math.pow(dy - center, 2)
);
const sinValue = (dy - center) / centerDistance;
const cosValue = (dx - center) / centerDistance;
dx = center + radius * cosValue;
dy = center + radius * sinValue;
const radians = Math.atan2(dy - center, dx - center);
const angle = (radians + Math.PI) / (Math.PI * 2);
setAngle((angle + 0.75) % 1);
setOffset({ dx, dy });
updateCursor();
};
const handleTouchEnd = () => {
document.removeEventListener("touchmove", handleTouchMove);
document.removeEventListener("touchend", handleTouchEnd);
resetCursor();
};
document.addEventListener("touchmove", handleTouchMove);
document.addEventListener("touchend", handleTouchEnd);
},
[node, dx, dy]
);
const updateCursor = () => {
document.body.style.cursor = "move";
document.body.style.userSelect = "none";
};
const resetCursor = () => {
document.body.style.removeProperty("cursor");
document.body.style.removeProperty("user-select");
};
React.useEffect(() => {
if (!node) return;
node.addEventListener("mousedown", handleMouseDown);
node.addEventListener("touchstart", handleTouchStart);
return () => {
node.removeEventListener("mousedown", handleMouseDown);
node.removeEventListener("touchstart", handleTouchStart);
};
}, [node, dx, dy]);
return [ref, dx, dy, angle];
};