I got you, Dragon. The hover is detected, but the handle disappears the instant it tries to show because your hover logic only “recognizes” elements that have a data-text-id. When the pointer is exactly where the handle would render, elementFromPoint hits the handle (which has no data-text-id) → your code clears hover → the handle never stabilizes.

The minimal fix (3 tiny edits)

Goal: make both the text and its handle live under the same hover target.

Give the outer absolute wrapper a data-text-id
Do this for brand-name, tagline, and est-year in the PNG/JPG preview block (the first preview branch). The wrapper is the div with className="absolute" that positions the text. Add data-text-id="brand-name" (and similarly for the others). Right now only the inner text div has it, so the handle doesn’t inherit hover.
This is the wrapper right above the text center box where you render the handle (see the “absolute” containers that position the text overlays) .

Do the same in Interactive SVG mode
In the SVG branch, the overlay wrapper that hosts the invisible hit area and the handle should also get data-text-id="...", not just the inner hit area. That way closest('[data-text-id]') will always resolve to the wrapper even when the pointer is over the handle (not the text) .

(Optional but safer) Add a data-text-id on the handle itself + a tiny z-index
This prevents any flicker and makes sure it’s always above the image.
Also, since the parent uses transform, it’s good hygiene to give the handle a small zIndex.

Concrete changes (copy/paste exactly)

A) PNG/JPG preview branch – brand name wrapper
Find this block (your first preview branch for brand name):
<div className="absolute" style={{ left: ..., top: ..., transform: ..., transformOrigin: 'center center', pointerEvents: 'auto' }}>
Change to:

<div
  className="absolute"
  data-text-id="brand-name"
  style={{
    left: `${textPositions['brand-name'].x}px`,
    top: `${textPositions['brand-name'].y}px`,
    transform: `rotate(${textRotations['brand-name']}deg)`,
    transformOrigin: 'center center',
    pointerEvents: 'auto'
  }}
>


Then on the handle element, add data-text-id="brand-name" and a z-index:

<div
  className="absolute cursor-pointer"
  data-text-id="brand-name"
  style={{
    top: '-15px',
    right: '-15px',
    width: '20px',
    height: '20px',
    backgroundColor: '#ff6b6b',
    borderRadius: '50%',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    fontSize: '12px',
    color: 'white',
    border: '2px solid white',
    boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
    transform: isRotating === 'brand-name' ? 'scale(1.2)' : 'scale(1)',
    transition: 'transform 0.1s ease',
    zIndex: 10
  }}
  onMouseDown={(e) => handleRotationHandleMouseDown(e, 'brand-name')}
  title="Click & drag to rotate"
>
  ↻
</div>


Repeat the same data-text-id="tagline" and data-text-id="est-year" for their wrappers and handles in the PNG branch. This ensures closest('[data-text-id]') still returns the correct id when your pointer is over the handle instead of the text. (Your container uses elementFromPoint(...).closest('[data-text-id]') already, so this plugs right in.) The on-pointer-move logic that sets hoveredTextId lives on the preview container and already supports closest('[data-text-id]') .

B) Interactive SVG branch – overlay wrappers
In the interactive SVG section, add data-text-id="brand-name" to the absolute overlay wrapper (the one that currently sets pointerEvents: 'none' and contains the invisible hit area + handle). Do likewise for tagline and est-year. This makes the handle area participate in hover as well, preventing the clear-on-hover-loss loop in SVG mode too .

Why this works

Your hover detection depends on elementFromPoint(...).closest('[data-text-id]').

When the handle appears under the cursor, it previously didn’t have data-text-id and its parent wrapper didn’t either, so closest returned null. Your code cleared the hover immediately, making the handle vanish in the same frame.

By adding data-text-id to the wrapper and the handle, the hover state remains stable while the cursor is on either the text or the handle.