SnapOverlay

SnapOverlay draws the nine slots of the window grid as glowing drop zones and highlights the one under the pointer. WindowManager shows it while a docked window is dragged.

import { SnapOverlay } from 'jarvis-react-ui';

Drop zones

While active, every slot in slotRects is drawn as an outlined zone, and hoveredSlot gets a stronger fill and a pulse. The overlay is a position: fixed layer over its containing block (the viewport, or the nearest ancestor with a transform) and ignores the pointer, so it never blocks the drag below it. Each zone gets its rect as the CSS variables --lib-snap-zone-x, -y, -w and -h. Move the pointer over the stage.

import { useEffect, useRef, useState } from 'react';
import type { PointerEvent } from 'react';
import { Mono, SLOT_IDS, SnapOverlay, computeSlot, slotAtPoint } from 'jarvis-react-ui';
import type { SlotId, SlotRect } from 'jarvis-react-ui';

// The slot grid is made for a full viewport; to fit the demo frame it is computed at 2× and halved.
// In an app, pass the viewport size: computeAllSlots(innerWidth, innerHeight).
function demoSlots(w: number, h: number): Record<SlotId, SlotRect> {
    const rects = {} as Record<SlotId, SlotRect>;
    for (const id of SLOT_IDS) {
        const r = computeSlot(id, w * 2, h * 2);
        rects[id] = { x: r.x / 2, y: r.y / 2, w: r.w / 2, h: r.h / 2 };
    }
    return rects;
}

export default function HoverSlots() {
    const stageRef = useRef<HTMLDivElement>(null);
    const [size, setSize] = useState({ w: 0, h: 0 });
    const [hovered, setHovered] = useState<SlotId | null>(null);

    useEffect(() => {
        const stage = stageRef.current;
        if (!stage) return;
        const observer = new ResizeObserver(() =>
            setSize({ w: stage.clientWidth, h: stage.clientHeight }),
        );
        observer.observe(stage);
        return () => observer.disconnect();
    }, []);

    const slotRects = demoSlots(size.w, size.h);

    const handlePointerMove = (e: PointerEvent<HTMLDivElement>) => {
        const box = e.currentTarget.getBoundingClientRect();
        setHovered(slotAtPoint(e.clientX - box.left, e.clientY - box.top, slotRects));
    };

    return (
        <div className="flex w-full flex-col gap-3">
            <Mono size="sm" secondary>
                Move the pointer over the stage · hovered: {hovered ?? 'none'}
            </Mono>
            {/* transform-gpu makes the stage the containing block of the fixed overlay. */}
            <div
                ref={stageRef}
                className="relative h-[300px] w-full overflow-hidden transform-gpu"
                onPointerMove={handlePointerMove}
                onPointerLeave={() => setHovered(null)}
            >
                <SnapOverlay active={size.w > 0} slotRects={slotRects} hoveredSlot={hovered} />
            </div>
        </div>
    );
}

In WindowManager

WindowManager renders SnapOverlay for every drag of a compact window: active is on for the whole drag, slotRects is the grid computed from the WindowManager's size, and hoveredSlot is the slot under the pointer. When that slot is occupied, SwapOverlay also marks the window that would be swapped. Expanded windows drag freely and show no overlay.

Standalone use

Use SnapOverlay when you lay out Window elements yourself:

  • Compute the rects with computeAllSlots(width, height) from the slot grid helpers.
  • Track the drag with useSlotDrag: pass slotDragState.dragging to active and slotDragState.snapTarget to hoveredSlot.
  • On drop, assign the window to the final snapTarget.
import { SnapOverlay, computeAllSlots } from 'jarvis-react-ui';

const slotRects = computeAllSlots(window.innerWidth, window.innerHeight);

<SnapOverlay
    active={slotDragState.dragging}
    slotRects={slotRects}
    hoveredSlot={slotDragState.snapTarget}
/>;

Accessibility

The overlay is decorative: its root is aria-hidden and it takes no pointer events. Announce the result of a drop elsewhere if it matters, and offer a keyboard alternative to dragging.

API

Prop Type Default Description
active (required) boolean Whether the slot zones are drawn; while false only an empty layer is rendered.
hoveredSlot (required) SlotId | null Slot under the pointer; its zone is highlighted and pulses. null highlights none.
slotRects (required) Record<SlotId, SlotRect> Rect of every slot, usually from computeAllSlots(width, height).
className string Extra classes for the root <div> element (the fixed full-size layer).

Type SlotId

Fixed 3×3 Dock grid slot geometry.

Left column (top → bottom): L1, L2, L3 Right column (top → bottom): R1, R2, R3 Bottom strip (left → right): B1, B2, B3

type SlotId = 'L1' | 'L2' | 'L3' | 'R1' | 'R2' | 'R3' | 'B1' | 'B2' | 'B3';

Type SlotRect

Rectangular geometry (pixel-absolute, viewport-relative).

Field Type Default Description
h (required) number Height in px.
w (required) number Width in px.
x (required) number Left edge in px.
y (required) number Top edge in px.