> For AI agents: the complete documentation index is available at https://gultyayev.github.io/ngx-virtual-dnd/llms.txt, the full documentation bundle is available at https://gultyayev.github.io/ngx-virtual-dnd/llms-full.txt.

***

title: How it works
description: Geometric hit-testing, placeholder math and rendering: what happens inside a drag.
-----------------------------------------------------------------------------------------------

# How it works

## The problem

Typical drag-and-drop libraries find the drop position by calling `getBoundingClientRect()` on sibling elements. A virtual list renders only the rows in view, so most siblings do not exist and that approach breaks.

## Geometric hit-testing

When a drag starts, ngx-virtual-dnd records the drop targets in the active group and their bounding rectangles. On each pointer move it compares the pointer position with those cached rectangles, instead of asking the browser which element is under the pointer. This avoids forcing a layout recalculation on every frame of the drag.

The cache updates itself:

- rectangles are re-read after scroll and resize, and when a target resizes (through `ResizeObserver`)
- targets mounted or removed during the drag are added or dropped
- each rectangle is clipped to its nearest `vdndScrollable` ancestor, so a list scrolled out of view inside a container cannot be hit

When rectangles overlap, the last one in document order wins.

## Placeholder math

Once the target list is known, the drop index comes from arithmetic, not DOM queries:

- The probe point is the dragged preview's center, capped at half an item below the preview's top edge, so tall items do not jump ahead of the pointer.
- **Fixed heights:** the index is `floor((probeY - listTop + scrollTop) / itemHeight)`.
- **Dynamic heights:** a binary search over prefix sums of measured heights finds the row under the probe. The placeholder only moves past that row once the preview's top edge crosses the row's midpoint, so a short item entering a tall row needs half of that row's height.

While an item is dragged within its own list it is hidden with `display: none` and the placeholder takes its slot. The list's total height stays the same, so content below the list does not shift. The index calculation accounts for the hidden item, so the logical position stays correct.

## Rendering

Pointer moves are coalesced into a single `requestAnimationFrame` loop that reads layout, computes the new state and then writes styles, in that order. Drag state lives in signals, so only the parts of the page that depend on it update. The loop and all high-frequency listeners run outside Angular's zone. The `dragStart`, `dragEnd` and `drop` outputs are emitted inside it, so handlers that change plain fields still render in zone.js apps.

## Row recycling

`<vdnd-sortable-list>` and `<vdnd-virtual-scroll>` can reuse the view of a row that scrolls out to render a row that scrolls in, and `*vdndVirtualFor` always does. See [Row recycling](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/performance.md#row-recycling) for what that means for your row components.

## Browser support

The latest versions of Chrome, Edge, Firefox and Safari, on desktop and mobile. Every change runs the end-to-end suite in Chromium, Firefox and WebKit, with mobile touch runs in Chromium and WebKit.
