useDraggableResizable
Move and resize engine for a fixed, bottom-right-anchored floating panel, with geometry saved in localStorage.
useDraggableResizable powers DraggableResizableModal. Use it to build a custom floating surface. The panel is positioned with position: fixed and right / bottom offsets. During a gesture, the hook writes motion values directly, so dragging and resizing do not re-render React.
Import#
import { useDraggableResizable, computeNextGeometry } from "@reactberry/system";
import type {
ResizeEdge,
PanelGeometry,
DraggableResizableBounds,
DraggableResizableOptions,
} from "@reactberry/system";
Usage#
"use client";
import { useCallback } from "react";
import { motion } from "motion/react";
import { useDraggableResizable } from "@reactberry/system";
export default function FloatingPanel({ children }: { children: React.ReactNode }) {
const getDefaults = useCallback(() => ({ width: 400, height: 300, right: 24, bottom: 24 }), []);
const { startMove, startResize, rightMV, bottomMV, widthMV, heightMV } = useDraggableResizable({
storageKey: "floating-panel",
getDefaults,
});
return (
<motion.div style={{ position: "fixed", right: rightMV, bottom: bottomMV, width: widthMV, height: heightMV }}>
<div onPointerDown={startMove()} style={{ cursor: "grab" }}>Drag me</div>
{children}
<div onPointerDown={startResize("se")} style={{ position: "absolute", right: 0, bottom: 0, width: 12, height: 12 }} />
</motion.div>
);
}
Examples#
Resize from any edge#
Attach startResize(edge) to a handle on each edge and corner, and pass bounds to change the minimum size. Hold Alt or Shift while resizing to resize around the centre.
API#
function useDraggableResizable(options: DraggableResizableOptions): {
geometry: PanelGeometry;
isResizing: boolean;
isMoving: boolean;
startResize: (edge: ResizeEdge, onCommit?: (g: PanelGeometry) => void) => (e: React.PointerEvent) => void;
startMove: (onCommit?: (g: PanelGeometry) => void) => (e: React.PointerEvent) => void;
resetPanel: () => void;
rightMV: MotionValue<number>;
bottomMV: MotionValue<number>;
widthMV: MotionValue<number>;
heightMV: MotionValue<number>;
}
PanelGeometry is { width, height, right, bottom }, all in pixels.
Parameters#
| Prop | Type | Default | Description |
|---|---|---|---|
options.storageKey* | string | — |
|
options.getDefaults* | () => PanelGeometry | — | Returns the geometry used when nothing valid is saved, and by |
options.bounds | Partial<DraggableResizableBounds> | — | Overrides for the bounds below. |
bounds.minWidth | number | 320 | Minimum width in pixels. |
bounds.minHeight | number | 240 | Minimum height in pixels. |
bounds.maxWidthVw | number | 0.98 | Maximum width as a fraction of the viewport width. |
bounds.maxHeightVh | number | 0.98 | Maximum height as a fraction of the viewport height. |
bounds.guard | number | 8 | Minimum gap in pixels to the left and top viewport edges. |
Returns#
| Prop | Type | Default | Description |
|---|---|---|---|
startMove | (onCommit?) => (event: React.PointerEvent) => void | — | Returns a |
startResize | (edge: ResizeEdge, onCommit?) => (event: React.PointerEvent) => void | — | Returns a |
rightMV | MotionValue<number> | — | Right offset. Bind it to the panel's |
bottomMV | MotionValue<number> | — | Bottom offset. Bind it to the panel's |
widthMV | MotionValue<number> | — | Width. Bind it to the panel's |
heightMV | MotionValue<number> | — | Height. Bind it to the panel's |
geometry | PanelGeometry | — | The last committed geometry. |
isMoving | boolean | — |
|
isResizing | boolean | — |
|
resetPanel | () => void | — | Sets |
When a gesture ends, the final geometry is passed to onCommit if given; otherwise it becomes geometry and is saved to localStorage.
computeNextGeometry#
function computeNextGeometry(args: {
edge: ResizeEdge;
start: PanelGeometry & { x: number; y: number };
dx: number;
dy: number;
alt: boolean;
shift: boolean;
viewport: { w: number; h: number };
bounds: DraggableResizableBounds;
}): PanelGeometry
The pure function the hook uses to calculate the size and position during a resize. The dragged edge moves while the opposite edge stays put. With alt or shift, the panel resizes symmetrically around its centre; shift on a side edge also resizes the other axis by the same amount.
| Prop | Type | Default | Description |
|---|---|---|---|
edge* | ResizeEdge | — | The edge or corner being dragged. |
start* | PanelGeometry & { x: number; y: number } | — | Geometry and pointer position when the gesture started. |
dx* | number | — | Horizontal pointer movement since the start, in pixels. |
dy* | number | — | Vertical pointer movement since the start, in pixels. |
alt* | boolean | — | Resize symmetrically around the centre. |
shift* | boolean | — | Resize symmetrically; on a side edge, also resize the other axis. |
viewport* | { w: number; h: number } | — | Viewport size in pixels. |
bounds* | DraggableResizableBounds | — | Size limits and edge guard. |
Accessibility#
- Moving and resizing are pointer-only; the hook handles no keyboard input.
- Resize handles have no role or label. Mark them
aria-hidden, as in the examples.
Notes#
- The saved geometry is clamped to the viewport on load and again when the window shrinks.
resetPanel()and the clamp on window resize updategeometry, but not the motion values. A panel styled with the motion values does not move until the next gesture.- Keep
getDefaultsstable (useCallbackoruseMemo);resetPanelchanges when it changes. - Gesture listeners are removed when the component unmounts.