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

@vixeq/react

v0.9.0

Published

React hooks for the Vixeq step sequencer engine.

Readme

@vixeq/react

React hooks for @vixeq/core.

Support

The current React peer range, TypeScript range, SSR expectations, and semver policy are documented in the repository support policy. @vixeq/react supports React >=18 <20.

import { useSequencerEngine } from "@vixeq/react";

const player = useSequencerEngine({ project, onStep });
await player.play();

Controlled Project Usage

import { createProject, toggleStep, type SequenceProject } from "@vixeq/core";
import { useSequencerEngine } from "@vixeq/react";
import { useState } from "react";

function Sequencer() {
  const [project, setProject] = useState<SequenceProject>(() => createProject());
  const player = useSequencerEngine({ project });
  const trackId = project.tracks[0].id;

  return (
    <>
      <button type="button" onClick={() => void player.toggle()}>
        {player.playbackState === "playing" ? "Pause" : "Play"}
      </button>
      <button type="button" onClick={() => setProject(toggleStep(project, trackId, 0))}>
        Toggle first step
      </button>
    </>
  );
}

The hook owns the SequencerEngine lifecycle and updates the engine when project changes. Continuous transport progress is exposed through positionRef.current and onPosition instead of per-frame React state. pendingOperation is the queued command head, and isBusy is true while a command is queued or running. Both sequencer and arrangement hooks expose transport-level controls: seekPositionMs, setPlaybackRate, and setTransportLoop. Arrangement also exposes setLoop for its local project loop behavior.

Audio-Synced Usage

Audio is optional. When you want a sequencer to follow an HTMLAudioElement, pass a transport from @vixeq/core.

import { createMediaElementTransport, createProject } from "@vixeq/core";
import { useSequencerEngine } from "@vixeq/react";
import { useMemo, useState } from "react";

function AudioSequencer() {
  const [project] = useState(() => createProject());
  const transport = useMemo(() => {
    const audio = new Audio("/loop.wav");
    audio.loop = true;
    return createMediaElementTransport(audio);
  }, []);
  const player = useSequencerEngine({ project, transport });

  return (
    <button type="button" disabled={player.isBusy} onClick={() => void player.toggle()}>
      {player.pendingOperation ? "Working..." : player.playbackState === "playing" ? "Pause" : "Play"}
    </button>
  );
}

Arrangement Usage

useArrangement mirrors useSequencerEngine's lifecycle (create on mount, dispose on unmount, hot-swap on prop change) for an ArrangementProject — a song-level structure with tempo-mapped timing, an explicit duration, and multiple patterns placed on a shared beat timeline.

import { createArrangement, createTimingMap } from "@vixeq/core";
import { useAnimatedChannels, useArrangement } from "@vixeq/react";

function Song() {
  const [arrangement] = useState(() =>
    createArrangement({
      timing: createTimingMap({ bpm: 120 }),
      durationBeats: 32,
      patterns,
      sections,
    }),
  );
  const player = useArrangement({ arrangement });

  // engine satisfies ChannelSource, so it composes directly with useAnimatedChannels
  useAnimatedChannels(player.engine, {
    onFrame: (values) => { /* write to DOM */ },
  });

  return (
    <>
      <button type="button" disabled={player.isBusy} onClick={() => void player.toggle()}>
        {player.playbackState === "playing" ? "Pause" : "Play"}
      </button>
      <button type="button" onClick={() => void player.seekBeat(16)}>Jump to chorus</button>
      <span>{player.currentSection?.id ?? "(gap)"}</span>
    </>
  );
}

projectError captures constructor/hot-swap failures (e.g. an invalid arrangement) without throwing during render. transportError captures playback command failures and command promises still reject.

Animated Channels

Drive CSS custom properties (or any per-frame sink) with a requestAnimationFrame loop, using either envelope-based or interpolation-based values.

Envelope mode

Pass a map of trackId → Envelope to trigger and sample beat-driven decay animations:

import { createDecayEnvelope } from "@vixeq/core";
import { bindChannelsToElement } from "@vixeq/core/dom";
import { useAnimatedChannels, useSequencerEngine } from "@vixeq/react";
import { useMemo, useRef } from "react";

const ENVELOPES = {
  [beatTrackId]: createDecayEnvelope({ decayRate: 4.5, impact: 1.0, lift: 0 }),
  [ctaTrackId]:  createDecayEnvelope({ decayRate: 2.0, impact: 0.8, lift: 0 }),
};

const CSS_MAPPING = {
  [beatTrackId]: "--pulse-beat",
  [ctaTrackId]:  "--pulse-cta",
};

function PulseScene() {
  const rootRef = useRef<HTMLDivElement>(null);
  const { engine } = useSequencerEngine({ project });
  const envelopes = useMemo(() => ENVELOPES, []);

  useAnimatedChannels(engine, {
    envelopes,
    onFrame: (values) => {
      if (rootRef.current) bindChannelsToElement(rootRef.current, values, CSS_MAPPING);
    },
  });

  return <div ref={rootRef} className="scene" />;
}

Interpolation mode

Without envelopes, the hook calls engine.sampleChannels(easing) each frame for smooth step-to-step morphing:

import { easeOutCubic } from "@vixeq/core";
import { useAnimatedChannels, useSequencerEngine } from "@vixeq/react";

function MorphScene() {
  const { engine } = useSequencerEngine({ project });
  const valuesRef = useAnimatedChannels(engine, {
    easing: easeOutCubic,
    onFrame: (values) => { /* write to DOM */ },
  });
  // valuesRef.current holds the latest { trackId: 0–1 } map
}

motionPreference

The hook follows prefers-reduced-motion by default. Pass motionPreference: "reduce" to stop the rAF loop and use static samples, or "no-preference" to keep the rAF loop running regardless of the OS setting:

import { useAnimatedChannels } from "@vixeq/react";

useAnimatedChannels(engine, { motionPreference: "reduce" });

Envelope mode requires a ChannelSource. It triggers envelopes from StepEvent.scheduledPositionMs, samples with engine.getPosition().positionMs, and resets envelopes on seek, stop, and affected Project changes. In reduced motion, ordinary step ticks are ignored, but explicit seek, stop, and Project changes still produce one fresh static sample.


This package is a thin React integration layer for the core engine. It does not include GUI, visualizer, shader, storage, or audio components.