Skip to main content

ns-ui

Dock Cursor Magnify

Cursor-proximity magnification row with Gaussian falloff: macOS-dock physics for any children.

Usage

registry/core/dock-cursor-magnify/demo.tsx
"use client";

import { MagneticDock } from "./component";

const ITEMS = ["N", "S", "U", "I", "◆", "▲", "●", "✦", "■"];

export default function MagneticDockDemo() {
  return (
    <div className="relative flex min-h-[420px] items-center justify-center overflow-hidden">
      <div
        aria-hidden
        className="absolute inset-0 [background-image:radial-gradient(circle,var(--border)_1px,transparent_1px)] [background-size:24px_24px]"
      />
      <div className="relative">
        <div className="relative rounded-md border border-black/10 bg-white/60 p-3 shadow-[0_16px_40px_-12px_rgba(0,0,0,0.35)] backdrop-blur-xl dark:border-white/10 dark:bg-white/[0.06]">
          <MagneticDock>
            {ITEMS.map((glyph) => (
              <button
                key={glyph}
                aria-label={`item ${glyph}`}
                className="flex h-12 w-12 items-center justify-center rounded-sm border border-black/10 bg-surface font-mono text-sm text-ns-muted transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent dark:border-white/10"
              >
                {glyph}
              </button>
            ))}
          </MagneticDock>
        </div>
        {/* ambient contact shadow — grounds the dock like a real taskbar */}
        <div
          aria-hidden
          className="absolute inset-x-6 -bottom-3 h-3 rounded-full bg-black/15 blur-md dark:bg-black/40"
        />
      </div>
    </div>
  );
}

Component

registry/core/dock-cursor-magnify/component.tsx
"use client";

import { useEffect, useRef, type ReactNode } from "react";

// Gaussian-falloff magnification on a direct-DOM rAF loop — no React state
// on the hot path, interruptible easing on enter/leave.
export function MagneticDock({
  children,
  gain = 0.55,
  sigma = 80,
  lift = 16,
  className = "",
}: {
  /** the dock's icons/items */
  children: ReactNode;
  /** max extra scale at zero distance (0.55 → 1.55x) */
  gain?: number;
  /** gaussian falloff radius in px */
  sigma?: number;
  /** upward shift in px at full magnification */
  lift?: number;
  /** extra classes merged onto the rendered root element */
  className?: string;
}) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

    let items: HTMLElement[] = [];
    let scales: number[] = [];
    let cursorX: number | null = null;
    let raf = 0;

    // re-sync from the live DOM whenever children are added/removed/reordered
    // — keeps the loop from animating a stale snapshot (and stale detached nodes).
    const sync = () => {
      const next = Array.from(container.children) as HTMLElement[];
      scales = next.map((el) => {
        const i = items.indexOf(el);
        return i === -1 ? 1 : scales[i];
      });
      items = next;
    };
    sync();

    const loop = () => {
      let settled = true;
      // untransformed centers (offsetLeft ignores transforms — no feedback wobble)
      const left = container.getBoundingClientRect().left;
      const centers = items.map((el) => left + el.offsetLeft + el.offsetWidth / 2);
      items.forEach((el, i) => {
        let target = 1;
        if (cursorX !== null) {
          const d = cursorX - centers[i];
          target = 1 + gain * Math.exp(-(d * d) / (2 * sigma * sigma));
        }
        scales[i] += (target - scales[i]) * 0.18;
        if (Math.abs(target - scales[i]) > 0.002) settled = false;
      });
      items.forEach((el, i) => {
        // neighbors shove outward so grown items never overlap
        let shift = 0;
        for (let j = 0; j < items.length; j++) {
          if (j === i) continue;
          const push = (scales[j] - 1) * items[j].offsetWidth * 0.55;
          shift += j < i ? push : -push;
        }
        const s = scales[i];
        el.style.transform = `translate(${shift}px, ${-(s - 1) * lift}px) scale(${s})`;
        // magnification amount 0..1 — sells depth beyond scale/lift alone,
        // color-mix(currentColor) so the glow adapts to each theme automatically.
        const t = Math.max(0, (s - 1) / gain);
        if (t > 0.002) {
          el.style.filter = `brightness(${1 + t * 0.12})`;
          el.style.boxShadow = `0 ${4 + t * 10}px ${10 + t * 16}px -4px color-mix(in srgb, currentColor ${Math.round(15 + t * 25)}%, transparent)`;
        } else {
          el.style.filter = "";
          el.style.boxShadow = "";
        }
      });
      raf = settled && cursorX === null ? 0 : requestAnimationFrame(loop);
    };
    const wake = () => {
      if (!raf) raf = requestAnimationFrame(loop);
    };

    const onMove = (e: PointerEvent) => {
      cursorX = e.clientX;
      wake();
    };
    const onLeave = () => {
      cursorX = null;
      wake();
    };

    const observer = new MutationObserver(() => {
      sync();
      wake();
    });
    observer.observe(container, { childList: true });

    container.addEventListener("pointermove", onMove);
    container.addEventListener("pointerleave", onLeave);
    return () => {
      cancelAnimationFrame(raf);
      observer.disconnect();
      container.removeEventListener("pointermove", onMove);
      container.removeEventListener("pointerleave", onLeave);
    };
  }, [gain, sigma, lift]);

  return (
    <div
      ref={containerRef}
      className={`flex items-end gap-2 [&>*]:origin-bottom [&>*]:will-change-transform ${className}`}
    >
      {children}
    </div>
  );
}

The usage example imports ./component because it sits next to the component file in this repo. Installed, that file lands at components/ui/dock-cursor-magnify.tsx, so point the import there.

Use when a macOS-dock-style row that magnifies items under cursor proximity with Gaussian falloff; use for an icon/action dock or nav row, not for showing scroll/read position through a document.

Install

npx shadcn add https://design.helpmarq.com/r/dock-cursor-magnify.json

Ask AI

Point an assistant at this component's docs (llms-full.txt) with one click.

Claude, ChatGPT, Grok, and Perplexity open with the prompt already in. Gemini copies it to your clipboard first. Paste it in once the chat opens.

Build spec

A horizontal dock that magnifies its children based on cursor proximity with a Gaussian distance falloff: items scale up and lift as the pointer nears, neighbors swell progressively less, everything settles back with interruptible easing on leave. Direct-DOM rAF loop with no React state on the hot path, transform-origin bottom, static under prefers-reduced-motion.

Props

PropTypeDefaultDescription
childrenReactNode—the dock's icons/items
gain?number0.55max extra scale at zero distance (0.55 → 1.55x)
sigma?number80gaussian falloff radius in px
lift?number16upward shift in px at full magnification
className?string—extra classes merged onto the rendered root element