the clamp math is over-restricting so the logo’s transformed box is being forced into a tiny “safe” area (likely computed from the wrong rect), so it looks like it’s hiding behind the corner.

Here’s a precise prompt for Replit to fix it without undoing the wins:

Prompt — Fix clamp so #logo-root uses the real preview bounds (no micro-box)

Symptoms

After adding bounds, the logo gets stuck in a tiny area near a corner and looks cropped.

Drag works, but the allowed region is way too small.

Root cause (typical)

Clamp is computed against the wrong rectangle (card/inner padding/zero-sized node), or uses element half-sizes that don’t match the transformed size.

Using untransformed bbox for clamp, or mixing viewBox units with CSS pixels.

Do exactly this

Measure the correct box

The rect for clamping must be the inner preview element we attach drag to:

const previewRect = previewRef.current!.getBoundingClientRect(); // px


Do not use the card, outer container, or the SVG’s own rect.

Clamp using the transformed bbox

For a candidate move/scale, build the transform string with current rotationDeg and scale and the candidate tx/ty:

translate(txCandidate, tyCandidate) rotate(rotationDeg) scale(scale)


Apply temporarily to #logo-root, then read:

const r = root.getBoundingClientRect(); // px, transformed


Immediately revert if you’re mutating live, or do this on a cloned, off-screen root.

Edge correction from screen coords (simple + robust)

Compute the deltas needed to keep the transformed rect fully inside the preview:

let dx = 0, dy = 0;
if (r.left   < previewRect.left)   dx = previewRect.left - r.left;
if (r.right  > previewRect.right)  dx = Math.min(dx, previewRect.right - r.right);
if (r.top    < previewRect.top)    dy = previewRect.top - r.top;
if (r.bottom > previewRect.bottom) dy = Math.min(dy, previewRect.bottom - r.bottom);


Apply the correction to the candidate translate:

tx = txCandidate + dx;
ty = tyCandidate + dy;


Set the final transform once per rAF.

This avoids any math with half-widths in percent and guarantees we’re clamping the actual visual box the user sees.

Never shrink the allowed area

Do not subtract padding twice or use the overlay element size.

Keep a small inset (1–2% of preview min dimension) if you want a visual margin, but apply it to previewRect once:

const inset = Math.min(previewRect.width, previewRect.height) * 0.02;
const bounds = new DOMRect(
  previewRect.left + inset,
  previewRect.top  + inset,
  previewRect.width  - inset*2,
  previewRect.height - inset*2
);
// then compare r.* to bounds instead of previewRect


Scale clamp separately (don’t cause micro-box)

Max scale: min(2.0, previewRect.width / bbox.width, previewRect.height / bbox.height) using the untransformed bbox once.

Do not recompute a new “inner” limit each drag; only clamp translate from Step 3 when moving.

Sanity checks

Ensure preview element has real pixels (fixed height, e.g., 520px; overflow:hidden).

Ensure <svg> does not have hard width/height; it must have viewBox and preserveAspectRatio="xMidYMid meet".

Transform order on #logo-root:
translate(tx, ty) rotate(rotationDeg) scale(scale)

Acceptance

Logo can be dragged anywhere in the preview and pushes “against” the actual edges, not a tiny box.

Rotation/scale still work and do not change the allowed region except when hitting the real preview edge.

No overflow; no invisible logo.

Works the same in Replit preview and a normal Chrome tab.

If they want to sanity-check quickly, ask them to log the four numbers during a drag once:

previewRect (w,h)

transformed rect from root.getBoundingClientRect() (left, top, right, bottom)

the applied transform
If previewRect is large but the transformed rect already starts tiny, they’re clamping against the wrong rectangle—this plan corrects that.