TransformBox

4 parts

A 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

Loading demo…

Anatomy

Import the parts and compose them. Each part forwards attributes to its underlying element.

vue
import {
  TransformBoxRoot,
  TransformBoxHandle,
  TransformBoxRotateHandle,
  TransformBoxStatus,
} from '@robonen/primitives/canvas/transform-box';

<TransformBoxRoot>
  <TransformBoxHandle />
  <TransformBoxRotateHandle />
  <TransformBoxStatus />
</TransformBoxRoot>

API Reference

TransformBoxRoot

Root

A 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).

PropTypeDescription
modelValue?TransformBoxValue | nullControlled transform (v-model). null resets to defaultValue.
defaultValue?TransformBoxValueUncontrolled initial transform.
aspectRatio?number | nullLock the width/height ratio (width / height) during scaling. null disables the lock; corners still aspect-lock while Shift is held.
rotationSnap?numberSnap rotation to multiples of this many degrees while Shift is held during a rotate drag. 0 disables rotation snapping.
allowFlip?booleanAllow corners/edges to flip past their anchor (negative size, mirrored). When false, an edge clamps at the minimum size instead of flipping.
minWidth?numberMinimum box width.
minHeight?numberMinimum box height.
pivot?TransformBoxPivotPivot that rotation and symmetric (Alt) resize anchor to: 'center' or a fractional { x, y } in [0, 1]² of the box.
keyboardStep?numberPer-keystroke move/resize step (Arrow keys).
keyboardLargeStep?numberLarger move step (Shift+Arrow when moving the body).
rotationStep?numberPer-keystroke rotation step in degrees (rotate handle).
selected?booleanWhether 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?booleanDisable all interaction.
dir?TransformBoxDirectionWriting direction. Omitted → inherited from the nearest ConfigProvider (falling back to 'ltr'); affects horizontal keyboard nudge sign.
Emits
EventPayload
update:modelValue[value: TransformBoxValue | null]
update:selected[value: boolean]

TransformBoxHandle

Handle

One 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.

PropTypeDescription
positionTransformBoxHandlePositionWhich edge or corner this handle controls.

TransformBoxRotateHandle

RotateHandle

The 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

Status

An 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.

PropTypeDescription
format?(value: TransformBoxValue) => stringFormat the announced string from the current transform. Defaults to a compact x, y, width × height, rotation° summary with rounded values.