Transform box
Select, move, scale and rotate clips on the preview.
<TransformBox.*> turns the preview into a canvas you can edit: click a clip to select it, drag it to move, drag a corner to scale and the top knob to rotate. Moves snap to the frame's edges and center. Each drag is one undo step.
Example
import { Preview, TransformBox } from "@miraiclip/react";
const ACCENT = "#38bdf8";
const CORNERS = [0, 1, 2, 3] as const;
// Inside <ProjectProvider> and <PlaybackProvider>.
export function CanvasEditor() {
return (
<div className="flex h-80 items-center justify-center bg-neutral-900">
<Preview fitPadding={24} className="bg-black">
{/* Children of <Preview> sit over the canvas, at its size. */}
<TransformBox.Root className="data-[dragging=move]:cursor-grabbing">
<TransformBox.Outline target="hover" stroke={ACCENT} strokeOpacity={0.5} />
<TransformBox.Guides stroke="#f472b6" strokeDasharray="4 3" />
<TransformBox.Outline stroke={ACCENT} strokeWidth={1.5} />
<TransformBox.RotateLine stroke={ACCENT} strokeWidth={1.5} />
{/* Body: drag inside the box to move. Render it before the handles so they stay on top. */}
<TransformBox.Body />
{CORNERS.map((corner) => (
<TransformBox.Handle
key={corner}
corner={corner}
className="h-2.5 w-2.5 rounded-sm border border-sky-400 bg-white"
/>
))}
<TransformBox.RotateHandle className="h-3 w-3 rounded-full border border-sky-400 bg-white" />
</TransformBox.Root>
</Preview>
</div>
);
}Anatomy
<Preview>
<TransformBox.Root> // the interaction layer over the canvas
<TransformBox.Outline target="hover" /> // the clip under the pointer
<TransformBox.Guides /> // snap lines while moving
<TransformBox.Outline /> // the selected clip
<TransformBox.RotateLine />
<TransformBox.Body /> // drag inside the box to move
<TransformBox.Handle corner={0} /> // … corners 0 to 3: scale
<TransformBox.RotateHandle /> // rotate
</TransformBox.Root>
</Preview>It must be inside <Preview>, which gives it the preview's size and scale. It renders nothing unless the preview is live, so in "fallback" or "unsupported" mode there is nothing to click. Render order is paint order: put the handles last so they stay on top of the body.
Parts
Root
A layer over the canvas (position: absolute; inset: 0). A press picks the topmost clip under the pointer and starts moving it right away; a press on empty space clears the selection.
| Prop | Default | Description |
|---|---|---|
snapPx | 6 | Moves snap to the frame's edges and center within this many screen pixels. 0 turns snapping off. Alt places freely |
dragThresholdPx | 2 | Movement below this is a click, not a drag |
nudge | true | Arrow keys move the selection 1 px, or 10 px with Shift. Keys typed into fields are left alone |
canSelect | visual clips on unlocked tracks | (clip) => boolean: which clips a click can pick |
onClipDoubleClick | (clipId) => void, for example to open a text editor |
data-dragging is "move", "scale" or "rotate" during a drag.
Outline
The selected clip's outline as an SVG polygon, following its rotation. target="hover" outlines the clip under the pointer instead. Props go to the <polygon>: stroke, strokeWidth, strokeDasharray, className.
Guides
Full-width and full-height lines for the frame edges or center the current move snapped to. Props go to each <line>.
Body
A transparent polygon over the selected clip that starts a move. Only rendered while the selection can be edited.
Handle
A corner handle (corner: 0 top-left, then clockwise) that scales the clip uniformly around its anchor. It is centered on the corner; size and style it with CSS. Its cursor follows the clip's rotation.
RotateHandle and RotateLine
The rotate knob, distance screen pixels (default 26) beyond the middle of the top edge, and the line joining it to the edge. Rotation snaps gently to right angles, or to 15° steps with Shift.
data-active is set on a handle while its drag is in progress.
Keyframes
A drag writes transform.x, y, scale and rotation. When a property has keyframes, the drag sets a keyframe at the playhead instead of the static value, so animated clips stay animated. The logic is beginTransformDrag and writeClipTransform in @miraiclip/editor.
Custom handles
useTransformBox() returns the state the parts use, for drawing your own:
| Field | Description |
|---|---|
box | The selected clip's bounds (composition px): corners, centerXPx, centerYPx, rotationDeg, scale; or null |
hover | Bounds of the clip under the pointer, when it isn't the selection |
guides | { x: number[], y: number[] }: the lines the move snapped to |
editable | The selected clip can be transformed (visual, track unlocked) |
dragging | "move", "scale", "rotate" or null |
scale, width, height | From the preview: multiply composition px by scale |
startDrag(event, mode) | Start a drag from your own element's onPointerDown |
import { useTransformBox } from "@miraiclip/react";
/** A side handle that scales, like a corner. Render it inside <TransformBox.Root>. */
function RightEdgeHandle() {
const { box, editable, scale, startDrag } = useTransformBox();
if (!box || !editable) return null;
const [, topRight, bottomRight] = box.corners;
return (
<div
className="absolute h-4 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white"
style={{
left: ((topRight.xPx + bottomRight.xPx) / 2) * scale,
top: ((topRight.yPx + bottomRight.yPx) / 2) * scale,
}}
onPointerDown={(e) => startDrag(e, "scale")}
/>
);
}