# matinee > Staged cursor performances for React. Version 0.1.2. A programmable ghost cursor. You script it, it performs against your real running app using real DOM events, and then it exports itself as a file you can commit. Package: matinee (npm) Repository: https://github.com/benhowdle89/matinee Demo: https://matinee.pages.dev License: MIT Peer dependency: react >=18 Runtime dependencies: none ## Install ```sh npm install matinee ``` ## Minimal complete example This is a working file. Nothing is elided. ```tsx import { useEffect } from 'react' import { Stage, useCursor } from 'matinee' import 'matinee/styles.css' function Demo() { const cursor = useCursor() useEffect(() => { void (async () => { await cursor.moveTo('#search') await cursor.type('#search', 'invoices from March') await cursor.click('#submit') await cursor.say('and there it is') })() }, [cursor]) return } export default function Root() { return ( ) } ``` Two required pieces: 1. Wrap the app in ``. It renders children untouched and mounts one fixed, pointer-events:none, aria-hidden overlay. It never affects layout. 2. Import `matinee/styles.css` once. Without it the cursor is invisible. `useCursor()` must be called inside a ``. It throws otherwise. ## Core concepts **The actor** is what performs. You reach it with `useCursor()`. **Targets** are how you say where. A target is one of: - a CSS selector string, for example `'#submit'` or `'.row:nth-child(2) button'` - a DOM `Element` - a React ref, meaning an object with a `.current` property - a point, `{ x: number, y: number }`, in viewport coordinates Targets are resolved **when the step runs, not when you write it**. A selector that does not exist yet is fine as long as it exists by the time the queue reaches it. If it still does not match, matinee logs a warning and the cursor stays put rather than throwing. **The queue.** Every method returns a promise that resolves when its motion finishes, and queues behind whatever is already running. This means `await` is optional and the order of your calls is always the order of execution. These two are equivalent: ```ts await cursor.moveTo('#a'); await cursor.click('#a') cursor.moveTo('#a'); cursor.click('#a') ``` **Real events.** `click` dispatches genuine `pointerdown`, `mousedown`, `pointerup`, `mouseup` and `click` events on the resolved element, so React `onClick` handlers fire. `type` writes through the prototype value setter and dispatches `input`, which is what makes React controlled inputs actually update. **Scripts.** Every performance records itself as plain JSON while it runs. `cursor.getScript()` returns it; `cursor.play(script)` replays it. ## Recipes ### Fill and submit a form ```ts await cursor.click('#email') await cursor.type('#email', 'ada@example.com') await cursor.type('#password', 'hunter2') await cursor.click('button[type="submit"]') ``` ### Scroll to something below the fold first ```ts await cursor.scrollTo('#pricing') await cursor.hover('#pro-plan', 800) await cursor.click('#upgrade') ``` ### Narrate with a speech bubble ```ts await cursor.say('first, search for the invoice') await cursor.type('#search', 'INV-0042') await cursor.say('and there it is', 2000) ``` ### Export an animated SVG for a README ```ts const svg = cursor.toSvg({ background: '#faf8f4', backdrop: '', width: 720, }) // write svg to a .svg file, commit it, then in markdown: // ``` ### Record the tab to WebM ```tsx const recorder = useRecorder() // recorder.start() triggers a browser permission prompt. This is unavoidable. // recorder.stop() then fills recorder.blob and recorder.url. // recorder.download('demo.webm') saves it. ``` ### Replay a saved performance ```ts const script = cursor.getScript() localStorage.setItem('demo', JSON.stringify(script)) // later, possibly in another session import { validateScript } from 'matinee' await cursor.play(validateScript(JSON.parse(localStorage.getItem('demo')))) ``` ## Behaviours that surprise people - **`toSvg()` exports the cursor, never your page.** matinee does not rasterise the DOM. With no options you get the cursor, nameplate and click ripples on a transparent background, intended to be laid over a screenshot you already have. To produce a standalone clip, pass `background` and a `backdrop` of your own raw SVG markup. - **A performance containing `scrollTo` cannot be exported coherently.** Recorded points are viewport coordinates and the export has no concept of a scroll offset, so points either side of a scroll do not relate to each other. Keep exportable takes scroll-free. - **`say()` is not rendered in SVG exports.** It shows live but the exporter treats it as a pause. - **The exported SVG contains no `