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

@sigx/motion

v0.1.0

Published

Animation primitives for SignalX on Lynx — spring/tween drivers built on AnimatedValue

Readme

@sigx/motion

Spring and tween animation drivers for SignalX on Lynx, built directly on SharedValue from @sigx/lynx. One customer of the cross-thread bridge alongside gestures and scroll.

The differentiator: animation progress is observable from the background thread for free. Mutate a SharedValue on MT via withSpring(sv, target) — the existing diff/publish bridge ships every frame to a BG-side sigx signal, so effect(() => sv.value) re-runs reactively. Neither framer-motion nor @lynx-js/motion offers this today; their value primitives are MT-only.

Installation

npm install @sigx/motion

Quick start

The animation tick runs on MT — all withSpring / withTiming / animate calls must sit inside a 'main thread' context. The simplest path is main-thread-bindtap with a 'main thread' directive on the handler:

import { component, useSharedValue, useAnimatedStyle, useMainThreadRef } from '@sigx/lynx';
import { withSpring, withTiming } from '@sigx/motion';

const App = component(() => {
  const x = useSharedValue(0);
  const boxRef = useMainThreadRef(null);
  useAnimatedStyle(boxRef, x, 'translateX', { factor: 1 });

  return () => (
    <view>
      <view main-thread:ref={boxRef} style={{ width: 60, height: 60, backgroundColor: '#facc15' }} />

      <view main-thread-bindtap={() => {
        'main thread';
        withSpring(x, 200, { stiffness: 200, damping: 20 });
      }}>
        <text>spring</text>
      </view>

      <view main-thread-bindtap={() => {
        'main thread';
        withTiming(x, 0, { duration: 0.4 });
      }}>
        <text>reset (tween)</text>
      </view>

      {/* BG-reactive — updates per animation frame for free */}
      <text>x = {x.value.toFixed(0)}px</text>
    </view>
  );
});

useAnimatedStyle(elRef, sv, 'translateX') is required for the bound element to actually move — withSpring only writes the SharedValue; the style binding registry (Phase 2.5) is what applies the transform.

API

animate(sv, target, options?)

Animate a SharedValue<number> toward target. Returns { stop, finished } controls. Marked 'main thread' — must be called from within a 'main thread' context.

const ctrl = animate(tx, 200, { type: 'spring', stiffness: 300, damping: 25 });
// later
ctrl.stop();
await ctrl.finished;  // resolves on completion or cancel

AnimateOptions extends both SpringOptions and TimingOptions:

interface AnimateOptions {
  type?: 'spring' | 'tween';      // default: 'spring' if no duration is set
  // spring physics
  stiffness?: number;             // default 100
  damping?: number;               // default 10
  mass?: number;                  // default 1
  velocity?: number;              // initial velocity, units/sec, default 0
  restSpeed?: number;
  restDelta?: number;
  // tween
  duration?: number;              // seconds, default 0.3
}

Default mode is spring. Pass { type: 'tween' } or { duration } to get a tween (always uses easeOut).

withSpring(sv, target, options?) / withTiming(sv, target, options?)

Promise-returning sugar. Use these when you don't need cancellation — they call animate() with the type pinned and return .finished.

await withSpring(tx, 200, { stiffness: 200, damping: 20 });
await withTiming(tx, 0, { duration: 0.4 });

Composition

Sigx-native idioms cover the patterns motion's onUpdate / onComplete callbacks would handle in a callback-shaped library — and they integrate cleanly with sigx's reactivity:

Per-frame side effects (replaces onUpdate):

withSpring(tx, 200);
// BG side — fires reactively per frame via the SharedValue bridge, zero extra wiring:
effect(() => updateUI(tx.value));

Run code on completion (replaces onComplete):

'main thread';
await withSpring(sv, 200);
runOnBackground(() => doNext())();

Concurrent animations:

'main thread';
await Promise.all([
  withSpring(x, 200),
  withSpring(y, 100),
  withTiming(opacity, 1, { duration: 0.3 }),
]);

Sequential / chained:

'main thread';
await withSpring(x, 200);
await withTiming(opacity, 0, { duration: 0.2 });

Mass cancellation (drop down to animate() for the controls handle):

'main thread';
const ctrls = [
  animate(a, 100),
  animate(b, 200),
  animate(c, 50),
];
// later: stop them all
ctrls.forEach((c) => c.stop());

The __FlushElementTree() calls each tick performs are coalesced via a microtask flag, so N concurrent animations produce one flush per frame, not N. Same pattern upstream's MTElementWrapper.flushElementTree uses.

spring(options) — solver factory

Underneath animate(). Exposed for advanced use (driving non-SharedValue values, mocking). Returns a { next(elapsedMs): { done, value } } solver. See src/spring.ts.

Easings

Built-in: linear, easeIn, easeOut, easeInOut, circIn, circOut, circInOut, backIn, backOut, backInOut, anticipate. Plus cubicBezier(x1, y1, x2, y2) and the mirrorEasing / reverseEasing modifiers.

Tween animations always use the built-in easeOut. Custom easing functions can't be passed directly: function references don't survive the worklet _c capture across the MT/BG bridge — they'd arrive on MT as undefined. If you need a non-built-in curve, either pick a different built-in or wait for registerEasing(name, fn) (filed as a follow-up in NEXT-STEPS.md).

Cancellation behavior

Each SharedValue has at most one in-flight animation. Calling animate() (or withSpring/withTiming) on a value that already has an animation in flight cancels the previous one before starting the new one. The previous animation's .finished promise still resolves (cancellation is not an error).

This matches motion's behavior and avoids the race where two ticks fight over the same value.

Tick scheduling

Animations tick via requestAnimationFrame. Lynx's worklet runtime installs globalThis.requestAnimationFrame on MT (Lynx SDK ≥ 2.16). Where rAF isn't available — older SDKs, Node test environments — @sigx/motion falls back to setTimeout(tick, 16) (≈60 fps).

Limitations / out of scope

  • Scalar SharedValue<number> only for v0.1. 2D values ({x, y}) need parallel animate() calls (one per axis) for now.
  • No velocity-carry across animations. When a new animate() cancels an in-flight one, the new one starts at velocity 0. Motion's MotionValue tracks velocity to support seamless gesture-to-spring handoff; sigx's SharedValue doesn't yet (would require extending SharedValueState<T> from { value } to { value, velocity }). Add iff a real use case needs it.
  • No duration→physics resolution (motion's findSpring). Spring options are physics-only (stiffness/damping/mass). { duration, bounce } not supported. Add iff users want it.
  • No keyframes / sequences / stagger. Spring + tween cover gesture-driven UI. The richer orchestration surface (variants, layout, presence, scroll-driven) lives in upstream @lynx-js/motion if you need it.

Why we ported instead of using upstream

@lynx-js/motion ships two entry points: . (full, ~200 LOC of thin wrappers around framer-motion + motion-dom) and ./mini (lean, ~250 LOC self-contained). Phase 2.6 spiked hosting motion-mini directly on top of sigx via a shim and reverted because the bring-up required eight cumulative pipeline concessions, two with silent-fragility risk (semantic-approximation useEffect/useMemo shims and an rspack sideEffects override of motion's metadata).

Porting motion-mini's algorithmic content (spring solver, easings, animate orchestration — ~250 LOC of Apache-2.0 math) was the cleaner path: faithful semantics we own, a sigx-shaped API, and the BG-observable SharedValue integration becomes first-class. See PHASE-2.6-LAYERING-PLAN.md "What we tried and reverted" for the spike write-up and PHASE-2.7-MOTION-PLAN.md "Why mini, not full motion" for the design rationale.

Attribution

Spring solver and easing functions are ported from @lynx-js/motion v0.0.3, motion-dom v12.23.12, and motion-utils v12.23.6 — all Apache-2.0. The cubic bezier code is in turn modified from Gaëtan Renaudeau's bezier-easing (MIT). Full attribution in THIRD_PARTY_NOTICES.md.

License

MIT (sigx adaptation). Ported portions remain under their upstream licenses (Apache-2.0 and MIT, see attribution above).