agentsclimarketplace

Input anticipation

Skill seangeng/skills/input-anticipation

Claude Code skills & agent prompts I actually reach for — copy-paste, no deps. Writeups at seangeng.com.

Install
npx -y skills add seangeng/skills --skill input-anticipation

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Implement anticipatory UI — interfaces that react to pointer intent *before* the click. Three patterns: a proximity focus ring that warms as the cursor nears a field, a magnetic target that leans toward the cursor, and trajectory prediction that arms/prefetches the element the cursor is heading toward. Use this skill whenever the user wants a UI to "feel alive / responsive / fast", asks for a glow or focus ring that reacts to the mouse, a magnetic or cursor-attracting button, hover-intent or predictive prefetch, "make the chat box feel premium", or mentions input anticipation / anticipatory design / reacting to cursor proximity. Also reach for it proactively when building a hero CTA, an AI chat input, or a primary nav and the user wants polish. It bakes in the performance budget (reserve it for 1-2 high-value targets, never every button) and the accessibility rules (pointer-only effects must stay pure decoration, never become the only way to reach a control), so the result is tasteful and safe rather than a gimmick.

SKILL.md

11.5 KB, as published. Nobody here has run it

Input anticipation

Most interfaces are reactive: the user clicks, the UI responds. Anticipatory interfaces react to intent — where the pointer is going — a beat before the interaction lands. Done on the right element it reads as "this feels expensive"; done on every element it's a laggy gimmick that hurts. This skill implements the three core patterns and, just as importantly, encodes when not to use them.

The whole family is built on one cheap signal: how far is the pointer from this element, and where is it heading? Everything else is turning that signal into opacity, transform, or a prefetch.

Before you build: the two rules that keep this tasteful

These aren't footnotes — they're why the effect works at all. Read them first.

1. Reserve it for 1–2 high-value targets. Every anticipating element runs geometry on every pointermove (dozens of events per second). One AI chat field or one hero CTA: imperceptible. Two hundred buttons each doing their own distance math: you've built a space heater and the page stutters. The effect also means less when everything does it — anticipation signals "this is the thing you came for." Apply it to the primary input/CTA/nav, not the whole page.

2. It's pointer-only, so it must be pure decoration — never function. Keyboard users, touch users, and screen-reader users have no cursor to anticipate, so they get none of this. That's fine as long as the underlying control works completely without it: the field still focuses, the button still clicks, the link still navigates. The trap is letting anticipation become the only path — e.g. a menu that opens only on predicted approach, or a control reachable only by sweeping a mouse near it. That locks out everyone not on a mouse. Rule: anticipation makes the fast path feel faster; it is never the only path. Also honor prefers-reduced-motion for anything that moves.

The shared signal: distance from a point to a rectangle

You almost never want distance to an element's center — you want distance to its nearest edge, which should read as 0 the instant the pointer is anywhere over the element. The clamp trick gives you exactly that:

function distanceToRect(x, y, rect) {
  const dx = Math.max(rect.left - x, 0, x - rect.right);
  const dy = Math.max(rect.top - y, 0, y - rect.bottom);
  return Math.hypot(dx, dy);
}

Why the Math.max(a, 0, b): if the pointer is left of the box, left - x is positive and wins; if right of it, x - right wins; if horizontally inside the box's span, both are negative and 0 wins — no horizontal gap. Same for dy. The result is the gap to the nearest edge, and 0 anywhere inside.

Turn distance into a 0→1 "intent" with a falloff curve:

function intentFrom(distance, radius = 180, exponent = 2) {
  return Math.max(0, 1 - distance / radius) ** exponent;
}

radius is how far away the element starts noticing the pointer. exponent shapes the response: ^1 is linear and feels mushy (the element reacts to faraway movement); ^2 keeps it quiet until the pointer is committed, then ramps fast. The squaring is doing the design work — it makes the response non-linear so distant motion is ignored and near motion is amplified. Default to ^2.

Performance: never drive the hot path through framework state

A pointermove handler fires constantly. Re-rendering a component on every event will cost frames. Read layout and write style straight to the DOM node; reserve state for things that change rarely (slider values, enabled/disabled). Read getBoundingClientRect() inside the handler (it reflects scroll); if you cache it, invalidate on scroll/resize.

Pattern 1 — Proximity focus ring

A ring/glow whose opacity tracks pointer distance. The canonical use is an AI chat input that "wakes up" as you approach. Vanilla:

const field = document.querySelector(".field");   // the element to sense
const ring  = document.querySelector(".ring");    // decorative overlay, aria-hidden

addEventListener("pointermove", (e) => {
  const d = distanceToRect(e.clientX, e.clientY, field.getBoundingClientRect());
  ring.style.opacity = intentFrom(d, 180, 2);
});
.ring {
  position: absolute; inset: -4px; border-radius: inherit;
  pointer-events: none; opacity: 0;
  transition: opacity 90ms linear;            /* smooths the per-event jumps */
  box-shadow: 0 0 0 1px hsl(217 91% 60% / .6),
              0 0 24px 2px hsl(217 91% 60% / .45);
}

Mark the ring aria-hidden="true" — it's pure decoration. Scope the listener to a container instead of window when you have several on a page, and reset to 0 on pointerleave. You can drive more than opacity off the same intent: warm a Send button's border, lift a placeholder. Keep it subtle.

React note: keep intent out of state. Hold refs to the field and ring, write ringRef.current.style.opacity in the handler. Use state only for tunables like radius/exponent, and mirror them into refs so the handler reads current values.

Pattern 2 — Magnetic target

The element leans toward the cursor as it approaches, so the click region effectively grows in the direction of travel. Same proximity value, applied to a transform:

const btn = document.querySelector(".magnetic");
const RANGE = 140, MAX = 14;                       // px sensing radius, px travel

btn.addEventListener("pointermove", (e) => {       // listen on a zone around it
  const r = btn.getBoundingClientRect();
  const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
  const pull = Math.max(0, 1 - Math.hypot(e.clientX - cx, e.clientY - cy) / RANGE);
  const tx = ((e.clientX - cx) / RANGE) * MAX * pull * 2;
  const ty = ((e.clientY - cy) / RANGE) * MAX * pull * 2;
  btn.style.transform = `translate(${tx}px, ${ty}px) scale(${1 + pull * 0.06})`;
});
btn.addEventListener("pointerleave", () => { btn.style.transform = ""; });
.magnetic { transition: transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1); }
@media (prefers-reduced-motion: reduce) { .magnetic { transition: none; } }

Keep MAX small (10–15px). Past that the button feels like it's dodging the cursor, which is the opposite of helpful. The overshoot easing (cubic-bezier(0.34,1.56,0.64,1)) makes the spring-back feel physical. Guard on prefers-reduced-motion — bail out of the handler entirely for users who asked the OS to reduce motion, since this one literally moves things.

Pattern 3 — Trajectory prediction (the useful one)

The strongest anticipation isn't proximity, it's heading: figure out which target the cursor is aimed at and arm it early — highlight it, or kick off the fetch its destination needs, during the ~200ms between intent and click.

Track a smoothed velocity, then for each candidate take the dot product of the heading with the direction to that candidate. A dot near 1 means "aimed straight at it." Require a forward cone so a glancing pass doesn't count, and add a gentle distance penalty so a nearer aligned target wins ties. When the pointer is nearly still there's no heading to read, so predict nothing — don't silently fall back to nearest-target, or every paused cursor lights something up.

Update velocity in the pointermove handler, but run the selection in a single requestAnimationFrame loop reading the latest velocity. That decouples the cost from event frequency and gives you one place to also draw a heading indicator.

let last = null, vx = 0, vy = 0, pointer = null;

zone.addEventListener("pointermove", (e) => {
  if (last) {
    const dt = Math.max(1, e.timeStamp - last.t);
    vx = vx * 0.7 + ((e.clientX - last.x) / dt) * 0.3;   // EMA, px/ms
    vy = vy * 0.7 + ((e.clientY - last.y) / dt) * 0.3;
  }
  last = { x: e.clientX, y: e.clientY, t: e.timeStamp };
  pointer = { x: e.clientX, y: e.clientY };
});

let predicted = null, armAt = 0;
function frame(t) {
  if (pointer) {
    const speed = Math.hypot(vx, vy);
    let best = null, bestScore = -Infinity;
    if (speed > 0.04) {                                   // moving → has a heading
      const nvx = vx / speed, nvy = vy / speed;
      for (const el of targets) {
        const r = el.getBoundingClientRect();
        const dx = r.left + r.width / 2 - pointer.x;
        const dy = r.top + r.height / 2 - pointer.y;
        const d = Math.hypot(dx, dy) || 1;
        const align = (dx / d) * nvx + (dy / d) * nvy;    // -1..1
        const score = align - d / 1600;                   // aligned + near wins
        if (align > 0.62 && score > bestScore) { bestScore = score; best = el; }
      }
    }
    if (best !== predicted) {                             // prediction moved
      highlight(predicted, false); highlight(best, true);
      predicted = best; armAt = t;                        // restart the hold timer
    } else if (best && t - armAt > 160) {
      prefetch(best);                                     // held → confidence → prefetch
    }
  }
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

The lesson the demo on seangeng.com learned the hard way: arm only the single current prediction, and clear it the instant the prediction moves. A "prefetched" flag that accumulates across targets ends with everything lit, which destroys the signal — the whole point is that one thing reacts. Make a real prefetch cache idempotent and invisible; keep the visible armed state tied to the live prediction. Reset everything on pointerleave.

This is the genuinely productive version: submenus that open along the path to them, links that prefetch when the user is clearly headed there, a search index that warms as the pointer drifts toward the box. Prefetch is a hint, not a commitment — debounce it (only fire once a target stays predicted for a breath) so a sweep across the row doesn't fire five fetches, and make sure the real click still works if the prefetch hasn't finished.

Putting it together / checklist

When you implement any of these, verify:

  • The control works fully without the effect (keyboard, touch, SR).
  • Applied to only 1–2 high-value targets, not sprinkled everywhere.
  • Hot path writes to the DOM directly; no per-event framework re-render.
  • Decorative overlays are aria-hidden; listeners reset on pointerleave.
  • prefers-reduced-motion disables anything that moves (patterns 2 and any animated arming in 3).
  • Prefetch (pattern 3) is debounced and never blocks the real action.

Credit: the proximity-ring formulation is from @gabriell_lab.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.