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

@aicut/react

v0.7.3

Published

React wrapper for the AiCut video editor + lighting picker — thin declarative shells over @aicut/core.

Readme

@aicut/react

React wrapper for the AiCut video editor — canvas timeline, custom toolbar slots, theming, i18n, drop-in <VideoEditor>.

npm License GitHub

AiCut editor

Install

pnpm add @aicut/react @aicut/core

Quick start

import { useRef } from "react";
import {
  VideoEditor,
  type VideoEditorApi,
  type Project,
} from "@aicut/react";
import "@aicut/core/styles.css";

const project: Project = {
  version: 1,
  sources: [
    { id: "s1", url: "/media/a.mp4", kind: "video", name: "a.mp4" },
  ],
  tracks: [{
    id: "t1",
    kind: "video",
    clips: [{ id: "c1", sourceId: "s1", in: 0, out: 5000, start: 0 }],
  }],
};

export function Editor() {
  const apiRef = useRef<VideoEditorApi | null>(null);
  return (
    <VideoEditor
      apiRef={apiRef}
      defaultProject={project}
      onChange={(p) => console.log("autosave", p)}
      onExport={(p) => fetch("/api/export", {
        method: "POST",
        body: JSON.stringify({ project: p }),
      })}
      style={{ height: 600 }}
    />
  );
}

The component is uncontrolled for project state — the editor owns the current project. To restore from JSON later:

apiRef.current?.setProject(savedJson);

Props

interface VideoEditorProps {
  defaultProject?: Project;

  theme?: Theme;                         // CSS-var overrides; reactive
  locale?: Partial<Locale>;              // EN default; pass localeZh for ZH

  headerLeft?: ReactNode;                // optional header row above
  headerRight?: ReactNode;               //   the preview — collapses
                                          //   when both are empty
  toolbarLeft?: ReactNode;               // host controls — left bookend
  toolbarRight?: ReactNode;              //                 right bookend

  playbackEngine?: PlaybackEngineFactory; // pluggable playback; default
                                           //   HtmlVideoEngine. Bound at
                                           //   mount — change + remount
                                           //   via `key` to re-apply.
  timelineHeight?: number;               // outer height of the bottom
                                           //   timeline area (default 240).
                                           //   Reactive; no remount needed.
  trackHeight?: number;                  // per-row height (default 56).
                                           //   Initial-only; process-wide.
  rulerHeight?: number;                  // time-label strip (default 24).

  apiRef?: Ref<VideoEditorApi | null>;

  onReady?: (api: VideoEditorApi) => void;
  onChange?: (project: Project) => void;
  onExport?: (project: Project) => void; // fired by api.requestExport()
  onTimeUpdate?: (ms: number) => void;
  onPlay?: () => void;
  onPause?: () => void;
  onSelectionChange?: (clipId: string | null) => void;
  onError?: (err: Error) => void;

  className?: string;
  style?: CSSProperties;
}

The apiRef value exposes the full EditorApiplay, pause, seek, split, trimLeft, trimRight, setProject, getProject, addSource, addTrack, removeClip, undo, redo, setTheme, setLocale, requestExport, and more. See @aicut/core for the complete surface.

Custom slots (header + toolbar)

The editor reserves four host-fillable slots — all empty by default with no chrome cost. The optional header above the preview auto-collapses when both header slots are empty, so the default layout is byte-for-byte identical to before they existed.

<VideoEditor
  // Header row above the preview — invisible when both null
  headerLeft={<strong>Untitled project</strong>}
  headerRight={
    <>
      <button onClick={share}>Share</button>
      <button onClick={() => apiRef.current?.requestExport()}>Export</button>
    </>
  }
/>

Toolbar bookends

<VideoEditor
  apiRef={apiRef}
  defaultProject={project}
  toolbarLeft={
    <select value={aspect} onChange={(e) => setAspect(e.target.value)}>
      <option value="16:9">16:9</option>
      <option value="9:16">9:16</option>
      <option value="1:1">1:1</option>
    </select>
  }
  toolbarRight={
    <button onClick={() => apiRef.current?.requestExport()}>Export</button>
  }
/>

api.requestExport() fires the export event with the current project JSON, which flows back through your onExport prop. Your handler decides whether to POST to a backend, download locally, etc.

Theming

<VideoEditor
  theme={{
    controlsBg: "#f6f6f8",
    controlsText: "rgba(0, 0, 0, 0.78)",
    controlsBorder: "rgba(0, 0, 0, 0.08)",
    controlsHover: "rgba(0, 0, 0, 0.06)",
    controlsActive: "rgba(0, 0, 0, 0.08)",
    previewBg: "#e4e4e7",      // letterbox colour around the video
  }}
  /* … */
/>

The theme prop is reactive — swap it any time and the editor calls setTheme internally.

i18n

import { VideoEditor, localeZh } from "@aicut/react";

// Whole-locale swap
<VideoEditor locale={localeZh} /* … */ />

// Partial override
<VideoEditor locale={{ undo: "Annuler" }} /* … */ />

locale is reactive too — runtime swap re-titles the toolbar and re-paints canvas labels in place.

Compact viewports

Default chrome is sized for desktop. For laptop side panels or embedded editors, shrink the bottom area to reclaim preview height:

const [timelineHeight, setTimelineHeight] = useState(160);

<VideoEditor
  defaultProject={project}
  timelineHeight={timelineHeight}   // reactive — drag a slider, the
                                    // editor recompacts in place
  trackHeight={40}                  // initial-only; needs remount to
                                    // change (key={trackHeight})
/>

Range guidance: timelineHeight ∈ [120, 480], trackHeight ∈ [28, 96], rulerHeight ∈ [18, 36]. The internal canvas scrolls vertically when tracks overflow.

Custom playback engine

The editor talks to playback through a single interface. The default is HtmlVideoEngine (one hidden <video> per source, swap on clip boundaries). To plug in a different one — WebCodecs, WebGL compositor, desktop-wrapper IPC bridge — pass a factory:

import {
  VideoEditor,
  type PlaybackEngineFactory,
} from "@aicut/react";

const myEngine: PlaybackEngineFactory = ({ host, project }) =>
  new MyCustomEngine(host, project); // implements PlaybackEngine

<VideoEditor
  defaultProject={project}
  playbackEngine={myEngine}        // initial-only — bound at mount
  /* … */
/>

PlaybackEngine, PlaybackEngineFactory, PlaybackEngineOptions, and the built-in HtmlVideoEngine are re-exported from @aicut/react so you don't need a separate @aicut/core import to write one.

See @aicut/core's playback section for the full interface contract.

WebCodecs engine (opt-in sub-entry)

For frame-accurate playback via the browser's VideoDecoder API, import from the sub-entry so mp4box.js (~200 KB) only loads when you ask for it:

import {
  WebCodecsEngine,
  isWebCodecsSupported,
} from "@aicut/react/webcodecs";

const factory = isWebCodecsSupported()
  ? (opts) => new WebCodecsEngine({ ...opts, debug: true })
  : undefined; // VideoEditor falls back to HtmlVideoEngine when undefined

<VideoEditor playbackEngine={factory} /* … */ />;

WebCodecsEngine v1 covers single-track MP4/MOV playback (H.264 / HEVC / VP9 / AV1 — whatever the browser's VideoDecoder supports). Multi-track compositing, audio, transitions land in follow-up releases.

Keyframes (panX / panY / scale animation)

Off by default. Flip the keyframes prop and all three playback engines (HTML5, Canvas, WebCodecs) start interpolating per-clip transforms between adjacent keyframes. Diamond markers appear on the timeline; drag them, edit values via the floating panel, snap them to each other.

const [kfEnabled, setKfEnabled] = useState(true);
const [edgeNav, setEdgeNav] = useState(true);

<VideoEditor
  defaultProject={project}
  keyframes={{ enabled: kfEnabled }}
  clipEdgeNav={{ enabled: edgeNav }} // adds the |◀ ▶| buttons + I/O shortcuts
  onKeyframeSelectionChange={(target) => console.log(target)}
  /* … */
/>

// Per-property mutators (panX / panY / scale animate independently).
apiRef.current?.addKeyframe("clip-1", "scale", { time: 0, value: 1 });
apiRef.current?.addKeyframe("clip-1", "scale", {
  time: 2000,
  value: 2.5,
  easing: "easeInOut",
});
apiRef.current?.setKeyframeValue("clip-1", kfId, 1.8);
apiRef.current?.setKeyframeEasing("clip-1", kfId, "easeOut");

// Toolbar-style "K at playhead" drops all 3 props at once.
apiRef.current?.setSelection("clip-1");
apiRef.current?.toggleKeyframeAtPlayhead();

Keyframe, KeyframeProp, EasingKind, EffectiveTransform, getEffectiveTransform, getTransformAtTimelineTime, IDENTITY_TRANSFORM, isIdentityTransform are all re-exported from @aicut/react for thumbnail / preview rendering outside the editor.

Backend export: both @aicut/backend-ts and @aicut/backend-go compile keyframes to ffmpeg t-expressions (scale=…:eval=frame + overlay=…:eval=frame). Pass output: { width, height, fps } in the export request — required for the keyframe filter graph to apply.

See @aicut/core's keyframes section for the full API surface.

<LightingEditor> (opt-in sub-entry)

A 3D lighting director for AI relighting flows — separate component that doesn't pull three.js into the rest of your bundle.

import { useRef } from "react";
import {
  LightingEditor,
  type LightingEditorApi,
  type LightingConfig,
} from "@aicut/react/lighting";
import "@aicut/core/styles.css";

function Relight() {
  const apiRef = useRef<LightingEditorApi | null>(null);

  const onGenerate = (): void => {
    const cfg = apiRef.current?.getConfig();
    if (cfg) fetch("/relight", { method: "POST", body: JSON.stringify(cfg) });
  };

  // Library renders ONLY the picker (scene + controls). Host lays out
  // the Smart mode panel beside it in their own flex/grid.
  return (
    <div style={{ display: "flex", gap: 16 }}>
      <LightingEditor
        apiRef={apiRef}
        subjectImageUrl="/frames/subject.jpg"
        onChange={(cfg: LightingConfig) => console.log(cfg)}
        // Reset / Generate / save-preset / etc. buttons go into the
        // controls column's footer slot — the only host-supplied
        // surface the library reserves space for.
        controlsFooter={
          <button onClick={() => apiRef.current?.reset()}>Reset</button>
        }
      />
      <aside>
        <textarea placeholder="Describe the mood…" />
        <button onClick={onGenerate}>Generate</button>
      </aside>
    </div>
  );
}

Props: subjectImageUrl, defaultConfig, defaultView, theme, locale, controlsFooter, onChange.

Imperative API (apiRef.current): setConfig, getConfig, setSubjectImage, setView, getView, reset.

The library is intentionally focused on the picker — Smart mode UI, Generate buttons, close handling, layout all live in host code.

Standalone <Timeline>

Use the canvas timeline without the rest of the editor — frame-pickers, thumbnail strips, read-only previews.

import { Timeline, type TimelineApi } from "@aicut/react";

<Timeline
  apiRef={timelineRef}
  defaultProject={singleClipProject}
  showHeader={false}
  readOnly
  toolbar                                            // 36px top strip
  toolbarLeft={<span>Picked at {ms / 1000}s</span>}
  toolbarRight={<button onClick={pick}>Use frame</button>}
  onSeek={(ms) => setPicked(ms)}
/>

Full docs & demo · @aicut/core · @aicut/vue