ReadTeardownsPricingDocsMCPOpen the chatRecipesWhat we never takeOpt out a siteChangelogStatusvorluno.dev
INSTALLnpm i @mvoom/sdk
LICENCEMIT
SOURCEthis page is the package README
OTHERS/docs/cli · /docs/mcp
DOCS / 03 · SDK

@mvoom/sdk

Turns a spec into real motion in production. Agnostic core, adapters per framework.

README

Mount a measured motion mechanism on real DOM. Zero dependencies, no framework, and the trigger is interpreted rather than approximated.

npm i @mvoom/sdk

The component you buy from mvoom is self-contained. It brings its own mounting code and depends on nothing, on purpose. This package is the other path: you have a VUI Spec — from the API or from the MCP connector — and you want to run it inside an app that already exists, without generating files.

That is the agent's path. A model asks for a mechanism and mounts it where the markup already is.

import { montar } from "@mvoom/sdk";

const detener = montar(receta, document.querySelector("#hero")!);
// later, when the component goes away
detener();

The trigger is the whole point

viewport-enter and scroll-scrub look identical in a single frame. They are not the same mechanism, and telling them apart cost the engine an entire pass.

TriggerWhat it does here
viewport-enterfires once when the element enters, then runs on the clock
scroll-scrubnever runs on its own — the scroll drives it, and it runs backwards when you scroll up
load loop timerruns immediately, on the clock
hover click focusthe person fires it
unknownmeasured, not attributable — the resting state is applied and nothing animates
unknown does not get a guessed trigger. Picking a reasonable-looking one would produce motion the original never had, and from the outside that is indistinguishable from a bad measurement. It is a result, not a gap.

The one thing that is not literally what was measured

A scrub's travel is re-anchored to your element. The engine measured a scroll range on the source page — say 400px to 2400px. That page is not the one your component lives in, so those numbers mean nothing here. What is preserved is the mechanism: which properties, on what curve, from where to where. The travel is re-anchored to the element's pass through the viewport. Everything else in this package is the measurement, unchanged.

Reduced motion

If the system asks for reduced motion, nothing animates and the final state is applied.

That distinction matters more than it sounds: an entrance that starts at opacity: 0 and simply does not animate leaves the content invisible forever. That is not respecting the preference, it is hiding the page.

// only for a test bench — never in production
montar(receta, el, { respetarPreferencia: false });

React and Vue

Thin adapters over the same core. Both are separate entry points, so if you use neither you do not even pull the import.

import { useMvoom } from "@mvoom/sdk/react";

export function Hero({ receta }) {
  const ref = useMvoom(receta);
  return <div ref={ref}>{/* elements carrying data-mv="…" */}</div>;
}
<script setup>
import { useMvoom } from "@mvoom/sdk/vue";
const raiz = useMvoom(receta);
</script>
<template><div ref="raiz"><!-- … --></div></template>

React and Vue are optional peer dependencies: the copy your app already has, never a second one. Two copies of React in a bundle do not produce a clear error — they produce hooks that fail somewhere unrelated.

Changing the options does not remount; changing the recipe does. Options are read once, because an object literal is new on every render and putting it in the dependency list would restart every animation on every render.

What this package does not do

Not includedWhy
MeasuringThat needs a real browser, five passes and a queue. This mounts what was already measured
Any network callIt takes a recipe you already have. It never phones home, and it has no idea a service exists
Generating codeThe purchased component does that, and it is self-contained. This is for running a spec in place
A scroll libraryThe scrub uses a passive scroll listener and the browser's own animation API. Smoothing is your app's decision, not ours

Zero dependencies, and why it matters here

This runs inside someone else's project. Every dependency is a decision imposed on them, and the whole file is short enough to read in one sitting — which is the only honest way to trust code that touches every element you point it at.

It validates the recipe before mounting, too. Types do not survive JSON.parse, and this receives data from the network: mounting a half-built object "as best it can" would produce motion nobody measured.

Support

VersioningSemantic, with a hand-written changelog
Securitysecurity@vorluno.dev — first response within 48 hours
LicenceMIT. The engine that produces the measurements is not — see ADR-MV016

<sub>Built and maintained by Vorluno Software, S.A. The mechanism this mounts was measured in a real browser, never inferred from a screenshot.</sub>

THE OTHER TWO