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

@kitlangton/animated-code

v0.2.0

Published

Morph one code block into another with stable tokens. Pure compiler plus a React renderer.

Readme

Animated Code

Morph one code block into another without losing your place.

A TypeScript library for animated code. Give it two (or more) versions of a snippet and it works out which lines and which words are the same, keeps those still, and crossfades the rest. A pure compiler produces frame data; a React renderer built on Motion plays it. MIT licensed. No runtime dependencies in the core.

Install

bun add @kitlangton/animated-code motion

React and Motion are peer dependencies of the /react entry. Import the stylesheet once.

Morph two snippets

import { AnimatedCode, useMorph } from "@kitlangton/animated-code/react";
import "@kitlangton/animated-code/styles.css";

function Example({ highlight }) {
  const [effect, setEffect] = useState(false);
  const data = useMorph([promiseVersion, effectVersion], { highlight });
  return (
    <>
      <button onClick={() => setEffect((v) => !v)}>Toggle</button>
      <AnimatedCode data={data} snapshot={effect ? 1 : 0} fontSize={16} />
    </>
  );
}

snapshot is the index of the version to show. Changing it animates every line to its new position and every changed word through a crossfade. Lines that only exist in one version fade and blur in or out. Any sequence of rapid toggles converges on the current snapshot: lines and parts never unmount, so there are no exit/enter races.

Bring your own highlighter

The library never owns a syntax highlighter. Pass a function from the full text of a frame to one row of tokens per line:

type Token = { text: string; color?: string; type?: string };
type Highlighter = (code: string) => Token[][] | Promise<Token[][]>;

Any Shiki setup fits (highlighter.codeToTokens(code).tokens mapped to { text: content, color }). Each distinct frame text is highlighted once; changed words are coloured in the context of the frame they belong to, so a keyword that becomes an identifier changes colour as it changes text.

renderToken on <AnimatedCode> swaps in a custom element for a token, for example to turn a // 1 marker comment into a numbered pill:

<AnimatedCode data={data} snapshot={0} renderToken={(token) => (token.type === "step" ? <Pill>{token.text}</Pill> : undefined)} />

Without React

import { compileMorph, morph, compile, highlightFrames, compress } from "@kitlangton/animated-code";

const data = compileMorph([a, b, c], { highlight });      // TemplateSlideshowData
const def = morph([a, b]);                                // AnimationDef: inspect or extend
const frames = highlightFrames(compile(def), highlight);  // SlideshowData: one Frame per snapshot

morph is biased towards keeping text. It pairs lines by exact match first, then by similarity inside each changed hunk, keeping the most shared characters (pairThreshold, default 0.3: const load = async () => { pairs with const load = Effect.gen(function* () {). Within a paired line it keeps every token common to all versions, weighted by length so a name outranks a keyword and down to a lone paren, so only async/=> ⇄ Effect.gen(/function* crossfade while const load =, () and { stay and slide. minAnchor (default 1) folds shorter matched runs into the change if you prefer fewer moving pieces. Indentation changes slide the line's text; } → }) keeps the brace and lets the paren enter.

Author an animation by hand

The morph is built on a small algebra you can use directly when a diff is not what you want to show:

import { compile, line, slot, stack, Mark, Spotlight, Cursor } from "@kitlangton/animated-code";

const animation = stack(
  line("sig", ["const user = ", slot("call", { promise: "await getUser(id)", effect: "yield* getUser(id)" })]),
  slot.optional("check", line("check", "if (!user) throw new NotFound(id)")),
  line("ret", "return user"),
);

const frames = compile({
  animation,
  snapshots: [
    { call: "promise" },
    { call: "promise", annotations: [new Spotlight({ patterns: ["await"] })] },
    { call: "effect", check: "shown", annotations: [new Mark({ text: "yield*" }), new Cursor({ text: "NotFound" })] },
  ],
});
  • text, line(id, content), stack, concat, slot(name, values), slot.hidden, slot.optional, section, empty, code(indent)`…`.
  • Annotations: Mark, Glow, Squiggle, Bright, Spotlight, Focus, Background, Status, Cursor, Say, Burst, Sound. Text-targeting annotations locate their range by pattern (text, nth, after, substring).
  • render(animation, snapshot) and toPlainText show what a snapshot spells.
  • analyzeStability(animation) warns when the same text hides inside every value of a slot and should be lifted out.

API

Core (@kitlangton/animated-code)

| Export | Purpose | | --- | --- | | morph(versions, options?) | AnimationDef with one snapshot per version | | compileMorph(versions, { highlight? }) | straight to TemplateSlideshowData; sync with a sync highlighter | | compile(def) | AnimationDef → SlideshowData frames | | highlightFrames(frames, highlight) | colour visible parts; returns a promise only if the highlighter does | | compress / expand / expandFrame | SlideshowData ⇄ TemplateSlideshowData | | render, toPlainText, getLineIds | inspect a snapshot as text | | analyzeStability, formatStabilityWarnings | authoring lint | | lcs, matchTokens, tokenize, similarity, alignLines | the diff primitives |

React (@kitlangton/animated-code/react)

| Export | Purpose | | --- | --- | | <AnimatedCode data snapshot fontSize? lineHeight? renderToken? className? style? /> | the renderer | | useMorph(versions, { highlight? }) | memoised compileMorph; resolves async highlighters after first paint | | LineParts, AnimatedPart, useFrame, useTextMeasurement, MeasurementsManager | building blocks |

Lines are positioned absolutely, so lineHeight must be a pixel number (default fontSize × 1.75). The block animates its own height.

Try it

git clone https://github.com/kitlangton/animated-code.git
cd animated-code
bun install
bun run dev

The demo morphs a Promise implementation into an Effect one, steps through three versions of a policy, and shows hand-authored slots with annotations.

Develop

bun run check            # typecheck, tests, package build
bun run smoke            # headless Chromium against the demo (needs Playwright's Chromium)
bun run bench:stability  # kept-character ratio per corpus pair (perf/morph-quality.md)

Vitest runs through bun run test. See AGENTS.md for the source map and invariants.