TransformBox
4 partsA headless move / scale / rotate bounding box. The root owns the transform
{ x, y, width, height, rotation } (controlled via v-model or uncontrolled
via defaultValue), sizes and rotates itself to it, and provides the gesture
machinery to its handle parts: TransformBoxHandle (8 scale handles),
TransformBoxRotateHandle, and the optional TransformBoxStatus live region.
All math (rotated-box resize, aspect lock, flip, rotation) lives in pure
helpers in ./utils so Crop can share resizeEdge/constrainRect/etc.
without importing any component. The body itself is draggable (move) and
keyboard-focusable (arrow-move); handles delegate their gesture math here.
Reach for it whenever a user repositions, resizes, or rotates a free object on
a canvas (image, shape, text frame, crop region).
Demo
Anatomy
Import the parts and compose them. Each part forwards attributes to its underlying element.
import {
TransformBoxRoot,
TransformBoxHandle,
TransformBoxRotateHandle,
TransformBoxStatus,
} from '@robonen/primitives/canvas/transform-box';
<TransformBoxRoot>
<TransformBoxHandle />
<TransformBoxRotateHandle />
<TransformBoxStatus />
</TransformBoxRoot>API Reference
TransformBoxRoot
RootA headless move / scale / rotate bounding box. The root owns the transform `{ x, y, width, height, rotation }` (controlled via `v-model` or uncontrolled via `defaultValue`), sizes and rotates itself to it, and provides the gesture machinery to its handle parts: `TransformBoxHandle` (8 scale handles), `TransformBoxRotateHandle`, and the optional `TransformBoxStatus` live region. All math (rotated-box resize, aspect lock, flip, rotation) lives in pure helpers in `./utils` so Crop can share `resizeEdge`/`constrainRect`/etc. without importing any component. The body itself is draggable (move) and keyboard-focusable (arrow-move); handles delegate their gesture math here. Reach for it whenever a user repositions, resizes, or rotates a free object on a canvas (image, shape, text frame, crop region).
| Prop | Type | Description |
|---|---|---|
modelValue? | TransformBoxValue | null | Controlled transform (v-model). null resets to defaultValue. |
defaultValue? | TransformBoxValue | Uncontrolled initial transform. |
aspectRatio? | number | null | Lock the width/height ratio (width / height) during scaling. null
disables the lock; corners still aspect-lock while Shift is held. |
rotationSnap? | number | Snap rotation to multiples of this many degrees while Shift is held during a
rotate drag. 0 disables rotation snapping. |
allowFlip? | boolean | Allow corners/edges to flip past their anchor (negative size, mirrored).
When false, an edge clamps at the minimum size instead of flipping. |
minWidth? | number | Minimum box width. |
minHeight? | number | Minimum box height. |
pivot? | TransformBoxPivot | Pivot that rotation and symmetric (Alt) resize anchor to: 'center' or a
fractional { x, y } in [0, 1]² of the box. |
keyboardStep? | number | Per-keystroke move/resize step (Arrow keys). |
keyboardLargeStep? | number | Larger move step (Shift+Arrow when moving the body). |
rotationStep? | number | Per-keystroke rotation step in degrees (rotate handle). |
selected? | boolean | Whether the box is selected/active (v-model:selected). A standalone box
defaults to selected; a multi-object editor binds this for click-to-activate. |
disabled? | boolean | Disable all interaction. |
dir? | TransformBoxDirection | Writing direction. Omitted → inherited from the nearest ConfigProvider
(falling back to 'ltr'); affects horizontal keyboard nudge sign. |
| Event | Payload |
|---|---|
update:modelValue | [value: TransformBoxValue | null] |
update:selected | [value: boolean] |
TransformBoxHandle
HandleOne of the eight scale handles on a `TransformBoxRoot` (four edges + four corners). Rendered as a native focusable `<button type="button">` so it is keyboard-reachable and announced; the default `aria-label` is derived from `position` (e.g. `"Resize top-left"`). Dragging resizes the edge/corner it controls while the OPPOSITE edge stays anchored in world space — even when the box is rotated, because the root rotates the screen delta into the box's local axes first. Hold Shift on a corner to lock the aspect ratio, Alt to resize symmetrically about the pivot. When `allowFlip` is `false` on the root, pushing past the anchor clamps at the minimum size instead of flipping. Keyboard (when focused): Arrow keys resize by the root's `keyboardStep`, Shift+Arrow aspect-locks, Alt+Arrow resizes symmetrically about the pivot.
| Prop | Type | Description |
|---|---|---|
position | TransformBoxHandlePosition | Which edge or corner this handle controls. |
TransformBoxRotateHandle
RotateHandleThe rotate handle of a `TransformBoxRoot`, rendered as a native focusable `<button type="button">` with a default `aria-label` of `"Rotate"`. Dragging it rotates the box about the root's `pivot`: the root computes the angle from the pivot to the pointer with `atan2` and accumulates the signed shortest delta so dragging across the 0°/360° seam stays smooth. Holding Shift snaps the rotation to the root's `rotationSnap` increments. Keyboard (when focused): Arrow Left/Down rotate by `-rotationStep`, Arrow Right/Up by `+rotationStep`; Shift+Arrow rotates by `rotationSnap` (when set). Position it yourself (commonly a stem above the top edge) via CSS; the default style centers it on the top-center of the box.
No props or events — renders its element and forwards attributes.
TransformBoxStatus
StatusAn optional visually-hidden `aria-live="polite"` region that announces the live numeric transform (x / y / width / height / rotation) to assistive technology when a gesture SETTLES — continuous drag feedback is otherwise purely visual. It mirrors the standard slider/spinbutton pattern of pairing a visual control with a text status. The announcement is debounced to a settle: it updates on the committed value (not every drag frame) so screen readers are not flooded. Override the wording with the `format` prop.
| Prop | Type | Description |
|---|---|---|
format? | (value: TransformBoxValue) => string | Format the announced string from the current transform. Defaults to a
compact x, y, width × height, rotation° summary with rounded values. |