import { useEffect, useRef, useState } from "react"; interface UseDragToScrollOptions { /** * Whether drag-to-scroll is enabled */ enabled?: boolean; /** * The direction to scroll */ direction?: "horizontal" | "vertical" | "both"; } /** * Hook to enable drag-to-scroll functionality on a scrollable element * @param options Configuration options * @returns Ref to attach to the scrollable element and mouse event handlers */ export function useDragToScroll({ enabled = true, direction = "horizontal", }: UseDragToScrollOptions = {}) { const scrollRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const startPosRef = useRef({ x: 0, y: 0 }); const scrollStartRef = useRef({ x: 0, y: 0 }); useEffect(() => { if (!enabled || !isDragging) return; const handleMouseMove = (e: MouseEvent) => { if (!scrollRef.current) return; const deltaX = e.clientX - startPosRef.current.x; const deltaY = e.clientY - startPosRef.current.y; if (direction === "horizontal" || direction === "both") { scrollRef.current.scrollLeft = scrollStartRef.current.x - deltaX; } if (direction === "vertical" || direction === "both") { scrollRef.current.scrollTop = scrollStartRef.current.y - deltaY; } }; const handleMouseUp = () => { setIsDragging(false); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); document.body.style.cursor = "grabbing"; document.body.style.userSelect = "none"; return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; }, [enabled, isDragging, direction]); const handleMouseDown = (e: React.MouseEvent) => { if (!scrollRef.current) return; const target = e.target as HTMLElement; const container = scrollRef.current; // Check if the click is on an interactive or draggable element // We need to be careful not to interfere with react-beautiful-dnd dragging const isInteractiveElement = target.closest("a") || target.closest("button") || target.closest("input") || target.closest("textarea") || target.closest("[role='button']") || target.closest("[draggable='true']") || target.closest(".react-beautiful-dnd-drag-handle") || target.closest("[data-rbd-drag-handle-draggable-id]") || target.closest("[data-rbd-draggable-id]"); // Don't start dragging if clicking on interactive elements if (isInteractiveElement) return; // Enable drag-to-scroll for any non-interactive element within the container if (container.contains(target)) { e.preventDefault(); setIsDragging(true); startPosRef.current = { x: e.clientX, y: e.clientY }; scrollStartRef.current = { x: container.scrollLeft, y: container.scrollTop, }; } }; return { ref: scrollRef, onMouseDown: handleMouseDown, isDragging, }; }