import { useRef, useState, useEffect, useCallback, type ReactNode } from "react"; import { LAYOUT } from "../constants"; interface DraggableContainerProps { children: ReactNode; initialPosition: "bottom-left" | "bottom-right" | { x: number; y: number }; className?: string; onDimensionsReady?: (dimensions: { width: number; height: number }) => void; } interface Position { x: number; y: number; } interface Dimensions { width: number; height: number; } const clampPosition = (position: Position, maxX: number, maxY: number): Position => ({ x: Math.max(0, Math.min(position.x, maxX)), y: Math.max(0, Math.min(position.y, maxY)), }); const getBasePosition = (position: "bottom-left" | "bottom-right", dimensions: Dimensions): Position => { const { width, height } = dimensions; switch (position) { case "bottom-left": return { x: LAYOUT.MARGINS.DEFAULT, y: window.innerHeight - height - LAYOUT.MARGINS.BOTTOM, }; case "bottom-right": return { x: window.innerWidth - width - LAYOUT.MARGINS.DEFAULT, y: window.innerHeight - height - LAYOUT.MARGINS.BOTTOM, }; } }; export default function DraggableContainer({ children, initialPosition, className = "", onDimensionsReady, }: DraggableContainerProps) { const [position, setPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const [hasBeenDragged, setHasBeenDragged] = useState(false); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [relativeOffset, setRelativeOffset] = useState({ x: 0, y: 0, }); const [dimensions, setDimensions] = useState({ width: 0, height: 0, }); const [isInitialized, setIsInitialized] = useState(false); const containerRef = useRef(null); const calculatePosition = useCallback((): Position => { if (!containerRef.current || dimensions.width === 0) return { x: 0, y: 0 }; if (typeof initialPosition === "object") { return initialPosition; } const basePosition = getBasePosition(initialPosition, dimensions); return hasBeenDragged ? { x: basePosition.x + relativeOffset.x, y: basePosition.y + relativeOffset.y, } : basePosition; }, [dimensions, initialPosition, hasBeenDragged, relativeOffset]); const updateDimensions = useCallback( (newDimensions: Dimensions) => { setDimensions(newDimensions); if (onDimensionsReady && !hasBeenDragged) { onDimensionsReady(newDimensions); } }, [onDimensionsReady, hasBeenDragged], ); const constrainToViewport = useCallback((pos: Position) => { if (!containerRef.current) return pos; const maxX = window.innerWidth - containerRef.current.offsetWidth; const maxY = window.innerHeight - containerRef.current.offsetHeight; return clampPosition(pos, maxX, maxY); }, []); useEffect(() => { if (!isInitialized && dimensions.width > 0 && !hasBeenDragged) { const newPosition = calculatePosition(); setPosition(constrainToViewport(newPosition)); setIsInitialized(true); } }, [isInitialized, dimensions.width, hasBeenDragged, calculatePosition, constrainToViewport]); useEffect(() => { if (!containerRef.current) return; const resizeObserver = new ResizeObserver(() => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const newDimensions = { width: rect.width, height: rect.height }; updateDimensions(newDimensions); setPosition((prev) => constrainToViewport(prev)); }); resizeObserver.observe(containerRef.current); return () => resizeObserver.disconnect(); }, [updateDimensions, constrainToViewport]); useEffect(() => { const handleResize = () => { const newPosition = calculatePosition(); setPosition(constrainToViewport(newPosition)); }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [calculatePosition, constrainToViewport]); useEffect(() => { if (!isDragging) return; const handleMouseMove = (e: MouseEvent) => { const newPosition = { x: e.clientX - dragOffset.x, y: e.clientY - dragOffset.y, }; setPosition(constrainToViewport(newPosition)); }; const handleMouseUp = () => { setIsDragging(false); setHasBeenDragged(true); if (containerRef.current && typeof initialPosition !== "object") { const basePosition = getBasePosition(initialPosition, dimensions); setRelativeOffset({ x: position.x - basePosition.x, y: position.y - basePosition.y, }); } }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [isDragging, dragOffset, position, initialPosition, dimensions, constrainToViewport]); const handleMouseDown = (e: React.MouseEvent) => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); setDragOffset({ x: e.clientX - rect.left, y: e.clientY - rect.top, }); setIsDragging(true); }; return (
{children}
); }