Skip to content

About

Threshold-based event detection for Motion MotionValue animations. Fire callbacks only on crossings, zero per-frame overhead.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

motion-cues

npm version npm downloads bundle size license

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 ^12.0.0 required. React ^19.0.0 optional (for /react).

Why

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

One threshold, fine. Three thresholds plus bidirectional travel? You're managing sorted arrays, direction flags, deduplication, cleanup — every time.

motion-cues does all of that for you.

Manual motion-cues
Threshold bookkeeping Flags per threshold Declarative defineCue
Direction detection Manual ascending/descending Automatic
Callback noise Every frame (~36 per 600ms) Crossings only (4–8)
Listeners One per concern One subscription, any cues
Bundle 0 ~1.2 kB gzipped

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}`),
});

stop(); // cleanup

React

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

function Progress() {
  const progress = useMotionValue(0);
  const flags = useCueState(progress, FLOOD);
  return <>{flags.half && <span>halfway</span>}</>;
}

Concurrent-safe (useSyncExternalStore), no useEffect, re-renders only on flips.

Examples

Staggered children

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

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

Custom ranges

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

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

Fan-out bus

const bus = createCueBus();
observeCue(progress, FLOOD, { bus });

bus.subscribe(() => {
  if (bus.has("flood", "half")) track("halfway");
  if (bus.has("flood", "full")) startNextPhase();
});

Conditional UI

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?)

const CUE = defineCue("my-cue", { first: 0.25, second: 0.5, third: 0.75 });
const SCROLL = defineCue("scroll", { visible: 200 }, { range: [0, 1000] });

observeCue(source, cue, options?)

Returns stop().

observeCue(motionValue, CUE, {
  initialSync: true,
  emitExitsOnStop: false,
  bus: myBus,
  onEnter: (key, ctx) => {},
  onExit: (key, ctx) => {},
});

ctx: value, progress (0–1), wasActive, direction ("ascending"|"descending"|"none"), velocity.

createCueBus()

const bus = createCueBus();
bus.has("cue-name", "threshold");
bus.get("cue-name");
bus.reset("cue-name");
bus.subscribe(() => {});

useCueState(source, cue, options?)

const flags = useCueState(motionValue, CUE);
// { first: boolean, second: boolean, ... }

License

MIT

About

Threshold-based event detection for Motion MotionValue animations. Fire callbacks only on crossings, zero per-frame overhead.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages