npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

motion-cues

v0.1.0

Published

Threshold cues on a `MotionValue` progress axis — callback API, no React.

Readme

motion-cues

Threshold-based event detection for Motion MotionValue animations.

Subscribe once. Get onEnter / onExit callbacks when a value crosses named thresholds. Zero per-frame overhead after setup.

npm install motion-cues

motion is a peer dependency. React (^19.0.0) is optional — only needed for /react.

Why This Exists

The Problem

When animating with Motion, you often need to trigger side effects at specific points along an animation — show an element when progress passes 50%, fire an accessibility announcement when a transition completes, stagger children based on how far along a drive value has traveled.

The naive approach uses Motion's .on("change", ...) listener:

progress.on("change", (v) => {
  if (v > 0.5 && !wasHalf) {
    // entered half
  }
  if (v < 0.5 && wasHalf) {
    // exited half
  }
  wasHalf = v > 0.5;
});

This works for one threshold. With three or four thresholds, per-element stagger, and bidirectional travel (open/close), the bookkeeping explodes. You need sorted thresholds, direction detection, deduplication, and cleanup logic — every time.

Why Not Just Use useEffect or watch?

  • useEffect / useMemo with dependency arrays — these fire on every render, not on animation frames. They can't observe MotionValue changes without bridging through state, which defeats the purpose of Motion's direct DOM updates.
  • motionValue.on("change", ...) directly — fires every frame. If you only care about threshold crossings, 95% of those callbacks are wasted work. You also need to track previous state, handle direction, and manage cleanup manually.
  • useTransform + useMotionValueEvent — closer, but still per-frame. You'd need to build the threshold logic yourself.

What motion-cues Does

  1. One subscriptionobserveCue registers a single .on("change", ...) listener internally.
  2. Sorted thresholds — thresholds are sorted once at creation. The hot path walks a pre-sorted array matching travel direction. Zero allocations per frame.
  3. Only fires on crossingsonEnter fires once when progress crosses a threshold upward. onExit fires once when it crosses downward. No per-frame noise.
  4. Clean APIdefineCue declares thresholds by name. observeCue subscribes. stop() cleans up.

Comparison

| Approach | Per-frame callbacks | Threshold bookkeeping | Direction awareness | Cleanup | Bundle cost | |----------|:---:|:---:|:---:|:---:|:---:| | motionValue.on("change", ...) | Every frame | Manual (per threshold) | Manual | Manual | 0 | | useEffect + state bridge | Every render | Manual | No | Automatic | 0 | | useTransform + useMotionValueEvent | Every frame | Manual | Manual | Manual | ~1 kB | | watch (Vue) / signal effects | Every change | Manual | No | Automatic | Varies | | motion-cues | Only on crossings | Declarative (defineCue) | Automatic | stop() | ~1.2 kB |

When to Use Each

| Scenario | Recommended | Why | |----------|-------------|-----| | Read current value once | motionValue.get() | No subscription needed | | Drive another MotionValue | useTransform | Built for value-to-value mapping | | Trigger side effects at thresholds | motion-cues | Fires only on crossings, not every frame | | Animate DOM directly every frame | motionValue.on("change", ...) | You need every intermediate value | | Conditional rendering based on animation progress | useCueState | Re-renders only when a threshold flips | | Multiple consumers of the same thresholds | motion-cues + createCueBus | One observer, many subscribers |

Performance

Allocation Profile

| Phase | Allocations | |-------|-------------| | defineCue | 1 object (the cue definition) | | observeCue setup | 2 sorted arrays (ascending + descending) + 1 flags object | | Per-frame (hot path) | 0 — reuses pre-sorted arrays, no object creation | | Threshold crossing | 1 context object (passed to callback) | | stop() | 0 (unless emitExitsOnStop: true, then 1 context per active cue) |

Why Zero Per-Frame Allocation Matters

A 60fps animation fires .on("change", ...) ~60 times per second. Over a 600ms transition, that's ~36 callbacks. If your handler creates objects, compares state, or branches on multiple thresholds, you're allocating and garbage-collecting on every frame — which can cause jank on low-end devices.

motion-cues front-loads all work:

defineCue("wave", { a: 0.1, b: 0.4, c: 0.7 })
  → sorts thresholds once: [a, b, c] (ascending) / [c, b, a] (descending)

observeCue(progress, wave, { onEnter, onExit })
  → registers ONE .on("change", ...) listener
  → per frame: compare value against pre-sorted array, skip if unchanged
  → fires callback ONLY when a threshold is crossed

Benchmark (Conceptual)

For a cue with 4 thresholds over a 600ms animation at 60fps:

| Metric | on("change", ...) | motion-cues | |--------|---------------------|---------------| | Callbacks fired | ~36 | 4–8 (only crossings) | | Objects allocated | 36+ (per handler) | 4–8 (context objects) | | Threshold comparisons | 144 (4 thresholds × 36 frames) | 36 (early exit on sorted walk) |

The difference is small for one cue. It compounds when you have multiple cues driving multiple elements — the stagger pattern in the examples section uses one cue with 3 thresholds across many children. With motion-cues, that's still one subscription.

Quickstart

import { useMotionValue } from "motion/react";
import { defineCue, observeCue } from "motion-cues";

const FLOOD = defineCue("flood", { half: 0.5, full: 1 });

const progress = useMotionValue(0);

const stop = observeCue(progress, FLOOD, {
  onEnter: (key) => console.log(`entered ${key}`),
  onExit: (key) => console.log(`exited ${key}`),
});

// progress.set(0.6)  → logs "entered half"
// progress.set(1.0)  → logs "entered full"
// progress.set(0.3)  → logs "exited full", "exited half"

stop(); // cleanup

React Usage

import { useMotionValue } from "motion/react";
import { defineCue } from "motion-cues";
import { useCueState } from "motion-cues/react";

const FLOOD = defineCue("flood", { half: 0.5, full: 1 });

function Progress() {
  const progress = useMotionValue(0);
  const flags = useCueState(progress, FLOOD);

  return (
    <>
      {flags.half && <span>halfway</span>}
      {flags.full && <span>done</span>}
    </>
  );
}

useCueState is backed by useSyncExternalStore — concurrent-safe, no useEffect, re-renders only when a threshold flips.

Examples

Staggered Children

Drive multiple children from a single progress value without per-key delays:

import { useMotionValue, animate } from "motion/react";
import { defineCue, observeCue } from "motion-cues";

const WAVE = defineCue("wave", { item0: 0.1, item1: 0.4, item2: 0.7 });

function StaggeredList() {
  const progress = useMotionValue(0);
  const [visible, setVisible] = useState([false, false, false]);

  useEffect(() =>
    observeCue(progress, WAVE, {
      onEnter: (key) => {
        const i = Number(key.replace("item", ""));
        setVisible((v) => v.map((s, j) => (j === i ? true : s)));
      },
    }),
  []);

  // Animate progress once — children appear at their thresholds
  const open = () => animate(progress, 1, { duration: 0.6 });
}

Accessibility Announcements

Announce state changes to screen readers at the right moment:

import { announcePolite } from "./a11y";
import { defineCue, observeCue } from "motion-cues";

const TRANSITION = defineCue("transition", {
  started: 0.05,
  complete: 0.95,
});

observeCue(progress, TRANSITION, {
  onEnter: (key) => {
    if (key === "complete") {
      announcePolite("Content loaded");
    }
  },
});

Custom Ranges

Cues work with any numeric range, not just [0, 1]:

// Scroll position in pixels
const SCROLL = defineCue("scroll", {
  headerHidden: 200,
  backToTopVisible: 600,
}, { range: [0, 1200] });

// Overlay position (100 → 0)
const OVERLAY = defineCue("overlay", {
  visible: 15,
  full: 0,
}, { range: [100, 0] });

Bus Fan-Out

Share one observer across multiple listeners:

import { createCueBus, defineCue, observeCue } from "motion-cues";

const bus = createCueBus();
const FLOOD = defineCue("flood", { half: 0.5, full: 1 });

// One observer, many subscribers
observeCue(progress, FLOOD, { bus });

// Subscriber A — drives animation
bus.subscribe(() => {
  if (bus.has("flood", "full")) startNextPhase();
});

// Subscriber B — drives analytics
bus.subscribe(() => {
  if (bus.has("flood", "half")) track("halfway");
});

Conditional UI Branching

Show different UI based on how far along an animation has progressed:

const INTERACTION = defineCue("interaction", {
  peek: 0.15,
  commit: 0.85,
}, { range: [100, 0] });

function EmailButton() {
  const overlayX = useMotionValue(100);
  const flags = useCueState(overlayX, INTERACTION);

  const variant = flags.commit ? "commit" : flags.peek ? "peek" : "idle";

  return <button className={variant}>Email Me</button>;
}

API

defineCue(name, schema, options?)

Create a named cue with threshold values.

const CUE = defineCue("my-cue", {
  first: 0.25,
  second: 0.5,
  third: 0.75,
});

// Custom range (default is [0, 1])
const SCROLL = defineCue("scroll", { visible: 200 }, { range: [0, 1000] });

observeCue(source, cue, options?)

Subscribe to threshold crossings. Returns stop().

const stop = observeCue(motionValue, CUE, {
  initialSync: true,  // fire onEnter for already-active thresholds (default: true)
  emitExitsOnStop: false, // fire onExit for all active cues when stop() is called
  bus: myBus,         // optional fan-out bus
  onEnter: (key, ctx) => { /* threshold crossed upward */ },
  onExit: (key, ctx) => { /* threshold crossed downward */ },
});

stop(); // unsubscribe

The ctx object provides: value, progress (normalized 0-1), wasActive, direction ("ascending" | "descending" | "none"), and velocity.

createCueBus()

Shared store for fan-out. Pass bus to observeCue, then subscribe.

const bus = createCueBus();
bus.has("cue-name", "threshold"); // boolean
bus.get("cue-name");              // { triggered: Set<string> }
bus.reset("cue-name");            // clear all triggered keys
bus.subscribe(() => { ... });     // returns unsubscribe fn

useCueState(source, cue, options?)

React hook. Returns Record<K, boolean> of active thresholds.

const flags = useCueState(motionValue, CUE);
// flags.first  → boolean
// flags.second → boolean

Backed by useSyncExternalStore. No useEffect. Re-renders only on threshold flips.

Compatibility

| Package | Range | | -------- | --------- | | motion | ^12.0.0 |

React (^19.0.0) is an optional peer — only needed for /react.

Versioning

Pre-1.0: minor bumps may break; patches are additive/fixes.

License

MIT