MynduiMyndui
0

Anatomy of the Dock

How a magnifying dock runs on one shared pointer signal instead of per-item mouse handlers, with each item's size chasing a distance-derived target on its own spring.

01/03One signal, many listeners
Pointer signal
one mouseX MotionValue, set on every mousemove
Dock items
each reads its own distance off that one value
tsx
const mouseX = useMotionValue(Number.POSITIVE_INFINITY);<divonMouseMove={(e) => mouseX.set(e.clientX)}onMouseLeave={() => mouseX.set(Number.POSITIVE_INFINITY)}><DockContext.Provider value={{ mouseX, baseSize, magnification, distance }}>  {children}</DockContext.Provider></div>
01

One signal, many listeners

A macOS-style dock looks like it needs a lot of coordination — every icon has to know how far the pointer is and how big its neighbors have grown. It doesn't. Dock publishes exactly one number, and every DockItem reads it independently. There's no loop that measures all items and repaints them; each one is its own small reactive system.

Dock holds a single mouseX MotionValue, updated on every mousemove and reset to +Infinity on mouseleave. It hands that value down through context — nothing else about pointer position is shared:

Resetting to Infinity rather than some off-screen coordinate matters: every item's distance-from-pointer calculation ends up huge and clamps to baseSize, so leaving the dock relaxes every icon back to rest in one write, with no separate "pointer left" branch to maintain.

02/03Distance to size spring

useSpring(sizeTarget, { mass: 0.1, stiffness: 170, damping: 12 })

Distance map
[-distance, 0, distance] → [baseSize, magnification, baseSize]
Spring
mass 0.1 · stiffness 170 · damping 12 chases the target size
tsx
const distanceFromMouse = useTransform(mouseX, (val) => {const bounds = ref.current?.getBoundingClientRect();if (!bounds) return distance + 1;return val - bounds.x - bounds.width / 2;});const sizeTarget = useTransform(distanceFromMouse,[-distance, 0, distance],[baseSize, magnification, baseSize],);const size = useSpring(sizeTarget, { mass: 0.1, stiffness: 170, damping: 12 });<motion.div style={{ width: size, height: size }} />
02

Distance to size spring

Each DockItem measures its own bounding box and subtracts the shared mouseX from its center to get a signed distance. useTransform maps that distance through a triangular curve — far away is baseSize, dead center is magnification, and it falls off linearly on either side — and a useSpring chases whatever that target becomes, frame by frame:

Defaults are baseSize: 44, magnification: 72, distance: 140 — an item 144px away from the pointer sits fully at rest; directly under it, it's 64% larger. Because sizeTarget recomputes from the live mouseX value every frame (not from React state), moving the pointer never triggers a re-render — the whole ripple runs on Framer's own animation loop, off React entirely. The low mass: 0.1 and damping: 12 spring is what gives the size a slight overshoot as it chases a fast-moving target, instead of snapping rigidly to it.

The tooltip above each item is a plain AnimatePresence child gated by CSS group-hover:block — it doesn't read mouseX at all. Framer only handles its opacity/y entrance; whether it's visible is pure CSS hover state on that item's own DOM node:

tsx
 
<AnimatePresence>
  {label && (
    <motion.span
      initial={{ opacity: 0, y: 4 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: 2 }}
      className="hidden group-hover:block ..."
    >
      {label}
    </motion.span>
  )}
</AnimatePresence>
03/03Result
03

The result

One mouseX value published on mousemove, one distance-to-size transform per item, one spring chasing that target — sweep the row and watch the ripple move with zero shared layout code between icons.

No useReducedMotion — worth knowing

Unlike most Myndui components, Dock doesn't check useReducedMotion anywhere in its source. The magnification spring runs unconditionally for every user. It's a deliberate gap worth calling out rather than papering over: if you're composing this into a page that needs to respect reduced motion, gate magnification down toward baseSize (or drop the useSpring entirely) yourself at the call site — the component won't do it for you.

Motion Score

DockSSCompositor-only
StranslatePosition / lift via translate
SopacityFade / cross-fade
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →