Index

Adaptive Precision

Most interfaces treat your cursor like a dumb dot. You point, it follows. The burden of precision falls entirely on you — your hand, your wrist, your patience. Miss a tiny checkbox by two pixels and nothing happens. The UI just sits there, indifferent.

But what if the interface adapted to you? What if, as your cursor approached a control, the control reached out and guided you to the right target?

That's the idea behind adaptive precision.

The demo

This is a time picker. Move your cursor into the grid and watch what happens. The cursor morphs from a circle into a bar that snaps to 15-minute slots. Click and drag to create an event.

1 PM
2 PM
3 PM
4 PM
5 PM

The interaction has two modes, and the transition between them is the whole point:

Outside the grid, the cursor is a soft circle. It follows your mouse freely with spring physics — no snapping, no constraints. You're just navigating.

Inside the grid, the cursor transforms into a thin horizontal bar that locks to the nearest time slot. Your coarse mouse movement becomes precise slot selection. The UI is doing the precision work for you.

How the snapping works

The core mechanic is simple. When the mouse enters the grid area, we calculate which 15-minute slot the cursor is closest to and snap to it:

const getSlotFromY = (clientY: number): number | null => {
  const rect = gridRef.current.getBoundingClientRect();
  const relY = clientY - rect.top;
  if (relY < 0 || relY >= GRID_HEIGHT) return null;
  return Math.floor(relY / SLOT_HEIGHT);
};

The slot index maps directly to a time. Slot 0 is 1:00 PM, slot 1 is 1:15 PM, slot 4 is 2:00 PM. Sixteen slots total, covering 1 PM to 5 PM.

When we detect the cursor is inside the grid, instead of passing through the raw mouse coordinates, we snap the spring target to the center of the matched slot:

if (isInsideGrid) {
  const slotCenterY = gridRect.top + slot * SLOT_HEIGHT + SLOT_HEIGHT / 2;
  mouseY.set(slotCenterY - containerRect.top);
}

Because this feeds into a useSpring, the cursor doesn't teleport — it glides to the slot boundary with physical momentum.

The cursor morph

The cursor is a single motion.div that animates between two shapes:

<motion.div
  animate={{
    width: isInGrid ? gridWidth : 28,
    height: isInGrid ? 4 : 28,
    borderRadius: isInGrid ? 2 : 14,
  }}
  transition={{
    type: "spring",
    stiffness: 400,
    damping: 40,
  }}
/>

Circle to bar. 28px to 4px tall. 28px to full-width. All through the same spring config, so the shape change feels like a single continuous motion rather than a swap.

The spring stiffness of 400 with damping 40 gives it a snappy feel without overshoot. It settles fast enough that slot-to-slot movement feels responsive, but slow enough that you can perceive the morph.

Drag to create

Click and drag creates a blue selection rectangle that spans from your start slot to wherever you drag. The selection snaps to slot boundaries too — you can't create a 7-minute event, only 15-minute increments.

The time range label updates live as you drag:

const startSlot = Math.min(dragStart, dragEnd);
const endSlot = Math.max(dragStart, dragEnd) + 1;
// "2 PM – 3:30 PM"

When the selection is tall enough, a "New Event" label fades in below the time range.

Why this matters

There's a fundamental tension in interfaces with fine-grained controls. A 15-minute time slot might only be 20 pixels tall. At normal mouse speed, that's hard to hit precisely. You slow down, overshoot, correct. It's a small friction, but it accumulates.

Adaptive precision resolves this by making the control snap-aware. The cursor's shape change is the key piece of feedback — it tells you "I understand what you're trying to do, and I'm helping." The transition from free movement to snapped precision feels natural because it's physically animated rather than instant.

This isn't a new idea in spirit. Scroll snapping, magnetic docking, even the way Figma snaps objects to guides — they're all forms of the interface adapting its precision to your intent. But making the cursor itself morph to communicate the mode change is what makes this version feel so considered.

The best interfaces don't just respond to input. They anticipate it.

"use client";
 
import { useState, useRef, useCallback, useEffect, useMemo } from "react";
import {
  motion,
  useMotionValue,
  useSpring,
  useTransform,
  AnimatePresence,
} from "motion/react";
 
const HOURS = [13, 14, 15, 16, 17];
const SLOTS_PER_HOUR = 4;
const TOTAL_SLOTS = (HOURS.length - 1) * SLOTS_PER_HOUR;
const SLOT_HEIGHT = 20;
const GRID_HEIGHT = TOTAL_SLOTS * SLOT_HEIGHT;
const GRID_PADDING_TOP = 56;
const GRID_PADDING_BOTTOM = 56;
const CONTAINER_HEIGHT =
  GRID_HEIGHT + GRID_PADDING_TOP + GRID_PADDING_BOTTOM + 20;
const CURSOR_SIZE = 28;
const BAR_HEIGHT = 4;
const HOUR_LABEL_WIDTH = 56;
 
function formatTime(slotIndex: number): string {
  const totalMinutes = 13 * 60 + slotIndex * 15;
  const hours24 = Math.floor(totalMinutes / 60);
  const minutes = totalMinutes % 60;
  const hours12 = hours24 > 12 ? hours24 - 12 : hours24;
  const suffix = hours24 >= 12 ? "PM" : "AM";
  if (minutes === 0) return `${hours12} ${suffix}`;
  return `${hours12}:${minutes.toString().padStart(2, "0")} ${suffix}`;
}
 
export function AdaptivePrecision() {
  const containerRef = useRef<HTMLDivElement>(null);
  const gridRef = useRef<HTMLDivElement>(null);
  const gridWidthRef = useRef(200);
 
  const [isInContainer, setIsInContainer] = useState(false);
  const [hoveredSlot, setHoveredSlot] = useState<number | null>(null);
  const [dragStart, setDragStart] = useState<number | null>(null);
  const [dragEnd, setDragEnd] = useState<number | null>(null);
  const [isDragging, setIsDragging] = useState(false);
  const [hasPointer, setHasPointer] = useState(true);
 
  const rawX = useMotionValue(0);
  const rawY = useMotionValue(0);
 
  const gridBlend = useMotionValue(0);
  const dragBlend = useMotionValue(0);
 
  const posSpring = { stiffness: 500, damping: 45, mass: 0.5 };
  const morphSpring = { stiffness: 380, damping: 34 };
 
  const cursorX = useSpring(rawX, posSpring);
  const cursorY = useSpring(rawY, posSpring);
  const blend = useSpring(gridBlend, morphSpring);
  const drag = useSpring(dragBlend, { stiffness: 300, damping: 30 });
 
  const cursorH = useTransform(blend, [0, 1], [CURSOR_SIZE, BAR_HEIGHT]);
  const cursorRadius = useTransform(blend, [0, 1], [CURSOR_SIZE / 2, 2]);
  const cursorOffsetY = useTransform(cursorH, (h) => -h / 2);
 
  const targetW = useMotionValue(CURSOR_SIZE);
  const cursorW = useSpring(targetW, morphSpring);
  const cursorOffsetX = useTransform(cursorW, (w) => -w / 2);
 
  const cursorBg = useTransform(
    [blend, drag],
    ([b, d]: number[]) => {
      const grayAlpha = 0.06 + b * 0.07;
      const blueAlpha = 0.12 + b * 0.15;
      const r = Math.round(d * 59);
      const g = Math.round(d * 130 + (1 - d) * 0);
      const bl = Math.round(d * 246 + (1 - d) * 0);
      const alpha = d * blueAlpha + (1 - d) * grayAlpha;
      return `rgba(${r},${g},${bl},${alpha})`;
    }
  );
 
  useEffect(() => {
    const unsubscribe = blend.on("change", (v) => {
      const w = CURSOR_SIZE + v * (gridWidthRef.current - CURSOR_SIZE);
      targetW.set(w);
    });
    return unsubscribe;
  }, [blend, targetW]);
 
  useEffect(() => {
    const measure = () => {
      if (gridRef.current) {
        gridWidthRef.current = gridRef.current.getBoundingClientRect().width;
      }
    };
    measure();
    window.addEventListener("resize", measure);
    return () => window.removeEventListener("resize", measure);
  }, []);
 
  useEffect(() => {
    const mq = window.matchMedia("(pointer: fine)");
    setHasPointer(mq.matches);
    const handler = (e: MediaQueryListEvent) => setHasPointer(e.matches);
    mq.addEventListener("change", handler);
    return () => mq.removeEventListener("change", handler);
  }, []);
 
  const getSlotFromY = useCallback((clientY: number): number | null => {
    if (!gridRef.current) return null;
    const rect = gridRef.current.getBoundingClientRect();
    const relY = clientY - rect.top;
    if (relY < 0 || relY >= GRID_HEIGHT) return null;
    return Math.min(Math.floor(relY / SLOT_HEIGHT), TOTAL_SLOTS - 1);
  }, []);
 
  const isInGridCheck = useCallback((clientX: number, clientY: number): boolean => {
    const gridRect = gridRef.current?.getBoundingClientRect();
    if (!gridRect) return false;
    return (
      clientX >= gridRect.left &&
      clientX <= gridRect.right &&
      clientY >= gridRect.top &&
      clientY < gridRect.top + GRID_HEIGHT
    );
  }, []);
 
  // ... mouse event handlers ...
 
  return (
    <div
      ref={containerRef}
      onMouseMove={handleMouseMove}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      className="adaptive-precision-container"
    >
      {/* Grid layout with hour labels and time slots */}
      {/* Custom cursor that morphs between circle and bar */}
      <motion.div
        animate={{ opacity: isInContainer ? 1 : 0 }}
        style={{
          position: "absolute",
          x: cursorX,
          y: cursorY,
          zIndex: 10,
          pointerEvents: "none",
        }}
      >
        <motion.div
          style={{
            width: cursorW,
            height: cursorH,
            borderRadius: cursorRadius,
            x: cursorOffsetX,
            y: cursorOffsetY,
            backgroundColor: cursorBg,
          }}
        />
      </motion.div>
    </div>
  );
}