If it only moves horizontally, that means the Y is updating in state but being neutralized by layout/CSS. Let’s fix it surgically without touching the rest.

Micro-fixes (do these in order)
1) Wrapper must be the positioning context

On the preview wrapper ([data-logo-preview]):

Ensure:

position: relative;

touch-action: none;

user-select: none;

(If it’s missing position: relative, absolutely-positioned children can’t place vertically.)

2) Draggable overlays must be truly absolute and not re-centered by a transform

On each draggable overlay (brand-name, tagline, est-year, shapes):

Ensure (or temporarily force) these styles:

position: absolute;

pointer-events: auto;

cursor: grab;

Temporarily set transform: none; (important)

Render with both left: ${xPct}% and top: ${yPct}%

If you currently have transform: translateY(-50%) or translate(-50%,-50%) on the overlay, it will visually pull the element back toward center on the Y axis, making it feel like it won’t move vertically. Kill those transforms for the draggable overlays. If you truly need centering later, we’ll restore it and adjust the math to compensate.

3) Pointer math: confirm Y is computed from the wrapper’s height

In your overlay pointer move handler for per-item drag, confirm all three lines exist exactly like this logic (conceptual — keep your function names):

const rect = wrapper.getBoundingClientRect() (wrapper = [data-logo-preview])

const dxPct = ((e.clientX - originX) / rect.width) * 100

const dyPct = ((e.clientY - originY) / rect.height) * 100 ← height, not width

next = { xPct: startXPct + dxPct, yPct: startYPct + dyPct } (with your clamp)

setPositions(prev => ({ ...prev, [dragId]: next })) inside requestAnimationFrame

If your code already looks like this, proceed to step 4.

4) Make sure you actually write top (not bottom) and you don’t mix units

When rendering overlays:

style={{ left: xPct + '%', top: yPct + '%' }}

Do not set bottom or use translateY here.

Do not render top in px while state stores %.

5) Remove the legacy text DOM listeners (the conflict)

Delete the useEffect that attaches mousedown on <text> and mousemove on document (the 644–729 block you pasted). Keep only the overlay/pointer system.

60-second smoke test

With transforms removed from overlays, drag brand-name → moves vertically now?

Drag tagline/est-year → both axes?

Drag a shape overlay → both axes?

If vertical now works, re-add any non-essential overlay transforms one by one; if adding a transform breaks vertical movement, we’ll adjust the math to account for it (e.g., apply a neutralizing translate only in render after computing top).

If it’s still only moving horizontally…

Two quick tells:

Y changes in state, but element doesn’t move: it’s CSS (a transform or not absolute/relative chain).

Y does not change in state: the move handler is using rect.width for both X and Y, or not writing top.

If you want, paste just your three handlers:

handleDragStart(e, dragId)

handleDragMove(e)

handleDragEnd(e)

…and the JSX for one overlay div (brand-name). I’ll give you a 3–5 line tweak, nothing else touched. 💜