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

@classytic/vixel-ui

v0.15.0

Published

Headless, configurable React editor primitives for vixel. A timeline UI over the VixelSpec contract: agent-emittable, human-editable, render via @classytic/vixel.

Downloads

842

Readme

@classytic/vixel-ui

Sponsor

Headless, configurable React editor primitives for vixel. A timeline + canvas editor over the VixelSpec contract — the same spec an agent emits, a human edits, and the vixel renderers export. Built for React 19.

One contract, two authors. The agent emits a VixelSpec (@classytic/vixel-schema); vixel-ui lets a human tweak the same spec; the server renders it. No second composition model, no drift — and the server export (@classytic/vixel-render-pixi) runs this package's own Pixi renderer, so preview == export by construction.

Why

  • 🎛️ Headless: no default styles, no shadow DOM. Render-prop + children-as-function, data-* styling hooks, CSS-variable theming. You own every pixel.
  • ⚡ React 19 native: useSyncExternalStore + selector subscriptions — a 60 Hz playhead re-renders nothing but the playhead.
  • 🖱️ Real editor interactions: multi-select (ctrl/shift/marquee), group drag, clipboard, a rebindable keyboard registry, frame-accurate transport.
  • 🖼️ Pro timeline visuals: cached audio waveforms + video filmstrips, headless or batteries-included.
  • 🧩 Capability-rich, client-configurable: every tool lives here; a mount enables a subset via features. Unused features tree-shake out.
  • 📜 Contract-first: edits a VixelSpec from @classytic/vixel-schema — the browser never pulls ffmpeg.

Install

npm install @classytic/vixel-ui @classytic/vixel-schema react react-dom

The ffmpeg engine (@classytic/vixel) is server-side only — install it where you render, not where you edit. In-browser MP4/GIF/PNG export ships here (WebCodecs, @classytic/vixel-ui/export).

Quick start

import { VixelEditor } from '@classytic/vixel-ui';
import { PixiPreview, TransformOverlay } from '@classytic/vixel-ui/preview';
import { Timeline, TimeRuler, Playhead, TimelineTrack, TimelineClip } from '@classytic/vixel-ui/timeline';
import { PlayButton, TimecodeDisplay } from '@classytic/vixel-ui/transport';
import type { VixelSpec } from '@classytic/vixel-schema';

export function Editor({ spec, onChange }: { spec: VixelSpec; onChange: (s: VixelSpec) => void }) {
  return (
    <VixelEditor spec={spec} onChange={onChange}>
      {/* Canvas — the SAME renderer the server export drives */}
      <div className="relative">
        <PixiPreview className="w-full" />
        <TransformOverlay /> {/* on-canvas move / resize / rotate */}
      </div>

      <div className="flex items-center gap-2">
        <PlayButton className="px-3 py-1 rounded bg-black text-white" />
        <TimecodeDisplay /> {/* frame-accurate M:SS.FF */}
      </div>

      <Timeline className="relative h-40 bg-neutral-900">
        {(tracks) => (
          <>
            <TimeRuler className="h-6" />
            {tracks.map((t) => (
              <TimelineTrack key={t.index} track={t} className="h-12">
                {(item) => (
                  <TimelineClip
                    item={item}
                    className="rounded bg-indigo-600 data-[selected=true]:ring-2 data-[primary=true]:ring-indigo-300"
                  />
                )}
              </TimelineTrack>
            ))}
            <Playhead className="w-px bg-red-500" />
          </>
        )}
      </Timeline>
    </VixelEditor>
  );
}

Prefer batteries-included? StandardEditor (from @classytic/vixel-ui, styled via CSS variables) mounts all of the above — including waveforms/filmstrips — in one component.

Applying derived styling (controlled spec — don't remount)

The spec prop is reconciling-controlled: the editor re-seeds only on a spec its store has never seen, and ignores the echo of its own onChange. Two correct ways to apply host-side transforms (theme/brand/background swaps), and one trap:

// ✅ In place, via the store — undo-labeled, media cache preserved.
const actions = useEditorActions();
actions.setSpec(restyleBackdrops(actions.getSpec(), brandFill));

// ✅ Controlled replacement — pass the transformed spec as the `spec` prop.
<VixelEditor spec={styledSpec} onChange={setSpec}>…</VixelEditor>

// ❌ NEVER key-remount to force a "refresh":
<VixelEditor key={styleVersion} spec={styledSpec}>…</VixelEditor>
// A remount destroys the editor instance AND its media cache — every image and
// video re-downloads and re-decodes. The two paths above re-render only the
// clips that actually changed.

Selection & editing

Selection is multi and id-keyed (it survives inserts/moves/undo by construction): plain click selects, ctrl/cmd-click toggles, shift-click selects a same-lane range, dragging empty timeline draws a marquee (ctrl = additive, Escape restores). Dragging a selected clip moves the whole selection.

Everything routes through one store (useEditorActions()):

const actions = useEditorActions();
actions.dispatch({ type: 'splitClip', clipId, atSec }); // typed, id-addressed command
actions.dispatchAll(commands, 'Apply agent edit'); // N commands → ONE undo entry
actions.deleteSelected(); // 'Delete 3 items' — one undo entry
actions.copySelected(); actions.pasteAt(); // relative layout preserved

dispatch/dispatchAll run the same pure reducer (applyCommand from @classytic/vixel-schema) an AI agent or server uses — one edit path for humans, agents, and telemetry. onSelect fires with the primary; onSelectionChange with the full set.

Template groups (compound clips)

Clips sharing a group tag (what applyTemplate stamps on every scene clip) collapse to one draggable/resizable block on the timeline — the CapCut template unit, not N stacked clips. The row model emits it as a kind: 'group' item, so track render-props branch:

{(item) => item.kind === 'group'
  ? <TimelineGroupClip item={item} />   // click = select members, dbl-click = edit
  : <TimelineClip item={item} />}

Click selects all members (Delete/copy act on the whole scene); a body drag commits ONE moveGroup; the right-edge handle commits ONE resizeGroup (proportional scene re-time — schema semantics). Double-click / Enter opens focused template-edit mode (actions.openGroup): the scene explodes into one row per element (templates build on one layered lane — inline members would overlap), top row = front-most. Each exploded row carries a host-ready TrackView.memberLabel (text content / source basename / shape kind) for gutters, and its item keeps its REAL spec position, so edits stay correct. Everything else is flagged locked (data-locked — dim it via CSS; all interactions early-return), and drags are time-only on the item's own lane while the mode is active. actions.closeGroup() exits; the mode auto-exits when the group is deleted or dissolved.

Keyboard shortcuts

One registry, scoped to the editor root (host inputs are never hijacked), every binding rebindable via the keymap prop, all off via features.shortcuts.

| | | | | | --- | --- | --- | --- | | Space play/pause | S split at playhead | Del delete selection | ⌘D duplicate | | ⌘A select all | Esc clear selection | ⌘C/X/V copy/cut/paste | ⌘Z/⇧⌘Z undo/redo | | ←/→ step 1 frame | ⇧←/→ step 1 s | ⌥←/→ nudge selection | Home/End start/end | | +/- zoom | | | |

Waveforms & filmstrips

Audio items render waveforms and video clips render filmstrip thumbnails in StandardEditor (toggle: features.mediaThumbnails). Building your own UI? The primitives are exported and cached (one decode per source, shared across clips, abort-safe, SSR-safe):

import { useWaveform, Waveform, useFilmstrip, Filmstrip } from '@classytic/vixel-ui';

const { peaks } = useWaveform(url);
<Waveform peaks={peaks} trimStartSec={item.in} style={{ color: '#818cf8' }} />;

const { tiles } = useFilmstrip({ url, trimStartSec, durationSec, height: 48 });
<Filmstrip tiles={tiles} />;

Preview & export

  • @classytic/vixel-ui/previewPixiPreview (retained Pixi scene, gl transitions, shader effects, masks, keyframes) + TransformOverlay (canvas gizmo publishing exact boxes).
  • @classytic/vixel-ui/export — in-browser MP4 (WebCodecs + AAC), GIF, and image export for shorts-length work.
  • @classytic/vixel-ui/renderer — the raw renderScene used by @classytic/vixel-render-pixi for WYSIWYG server export, and by hosts doing custom draw loops.

Features

| Flag | Gates | Default | | --- | --- | --- | | transitions | clip-to-clip transition UI | on | | kenBurns | zoom/pan animation UI | on | | captions | caption editing | on | | overlays | extra visual lanes | on | | multiTrackAudio | multiple audio lanes | on | | effects | WebGL effect surfaces | on | | shortcuts | the keyboard registry | on | | mediaThumbnails | waveforms + filmstrips in StandardEditor | on |

Subpath exports

| Subpath | Contents | | --- | --- | | @classytic/vixel-ui | VixelEditor, StandardEditor, store hooks, media primitives, keymap, types | | /editor | editor store / provider internals | | /timeline | Timeline, TimeRuler, Playhead, TimelineTrack, TimelineClip, TimelineGroupClip, TimelineMarquee, drag/marquee hooks | | /transport | PlayButton, TimeDisplay, TimecodeDisplay, PreviewSurface, ExportButton | | /preview | PixiPreview, TransformOverlay, preview audio | | /renderer | headless renderScene (server export / custom hosts) | | /export | WebCodecs MP4 / GIF / image export, audio-mix plan executor | | /shared | time / spec utilities, cn, cva variants | | /headless | the PORTABLE core — store, commands, keymap logic, clipboard/split planning, audio-mix plan (no DOM, CI-enforced; see PORTING.md) | | /driver | RenderDriver / FrameServer / AudioMixPlan porting contracts + createPixiRenderDriver() |

Theming — bring your own design system

The primitives are headless (positioning + interaction inline, zero visual opinion); StandardEditor + the cva variants are one reference skin. Hosts restyle at four standardized levels (shadcn/Radix conventions):

1. className + classNames slots. Every component takes className (root). Composites also take a classNames record of named parts, merged via cn (tailwind-merge) AFTER defaults — your classes win conflicts:

<StandardEditor
  spec={spec}
  classNames={{
    clip: 'bg-emerald-700 rounded-sm',      // beats the default bg-chart-1
    clipSelected: 'ring-amber-400',         // appended while selected
    track: 'h-16', playhead: 'bg-red-500', ruler: 'text-neutral-400',
  }}
/>
<TimelineClip item={item} classNames={{ root: '…', selected: '…', dragging: '…', trimStart: '…', trimEnd: '…' }} />

Slots per component: Timeline { root, marquee } · TimelineClip { root, selected, dragging, trimStart, trimEnd } · TimelineGroupClip { root, selected, dragging, trimHandle, label, duration } · TimeRuler { root, tick } · MarkerRail { root, marker } · KeyframeRail { root, marker } · TimelineTransitions { container, seam, handle, core } · PixiPreview { root, loading, spinner } · StandardEditor { root, toolbar, playButton, timeDisplay, exportButton, preview, timeline, ruler, track, clip, clipSelected, clipLabel, playhead, groupClip }.

2. State via data-attributes. Interactive state is exposed on the DOM, so Tailwind (data-[selected=true]:…) and plain CSS both target it:

| Element | Attributes | | --- | --- | | [data-vixel-clip] | data-kind, data-selected, data-primary, data-dragging, data-locked | | [data-vixel-group-clip] | data-group-id, data-selected, data-dragging, data-locked | | [data-vixel-playhead] | data-dragging | | [data-vixel-marquee] | data-active | | [data-vixel-transition] | data-selected, data-empty, data-dragging | | [data-vixel-keyframe] | data-active, data-dragging | | [data-vixel-play], [data-vixel-preview] | data-state="playing|paused" | | [data-vixel-pixi-preview] | data-loading, data-playing | | [data-vixel-track] | data-track-type, data-track-index, data-track-lane |

3. --vixel-* design tokens. Everything the package styles outside Tailwind (transform gizmo, marquee, preview loader) reads CSS custom properties with safe inline fallbacks — override at :root (or any wrapper) to retheme; the full token table with defaults lives at the top of styles.css. --vixel-primary falls back to the shadcn --color-primary, so a shadcn theme drives both the Tailwind skin and the inline affordances automatically. Importing styles.css stays optional — components are fully functional unstyled (enforced by the no-styles smoke test).

4. Render-prop slot overrides. Where a part is genuinely replaceable, the tree already has an escape hatch: children-as-function on TimelineClip / TimelineGroupClip / PlayButton / TimeDisplay / TimeRuler (ticks), renderMarker on MarkerRail / KeyframeRail, children(state) on TimelineTransitions.

Porting (React Native and beyond)

The editor core is enforced-portable (@classytic/vixel-ui/headless — a CI scan rejects any DOM leak) and the platform seams are named contracts (@classytic/vixel-ui/driver: RenderDriver, FrameServer, AudioMixPlan). A native app is a binding project — new UI + new drivers over the same store, commands, and plans — never a rewrite, and never a WebView. The full map: PORTING.md.

Packages

| Package | Role | | --- | --- | | @classytic/vixel-schema | the contractVixelSpec, validator, pure edit core | | @classytic/vixel-ui | this — headless editor + Pixi preview + browser export | | @classytic/vixel | server render engine (ffmpeg) | | @classytic/vixel-render-pixi | WYSIWYG server export (drives this package's renderer) | | @classytic/vixel-agent | agent tool surface (AI-SDK / MCP) over the same commands |

Releasing

npm test proves logic; it does not prove that a ten-minute long-GOP source survives hard scrubbing and resumes playing. That is measured by the real-browser benchmarks, which need fixtures too large to commit, so publishing runs them as a gate rather than skipping them:

ffmpeg -version                        # required to build the fixtures
node scripts/gen-hostile-fixtures.mjs  # once per checkout (~250 MB, gitignored)
npm run test:perf                      # the browser benchmark gate alone
npm run release                        # typecheck + tests + test:perf + build

prepublishOnly runs release, so a publish cannot skip the benchmark. If the fixtures or the browser driver are missing, test:perf fails with the command to run; it never degrades into a quiet skip. Run it on an otherwise unloaded machine: the assertions are wall-clock.

License

MIT © Classytic

Trademark

MIT-licensed code. "Classytic"/"arc" names + logos are trademarks of Classytic LLC — see TRADEMARK.md.