Yours after install
The registry ships the real source files. What lands in components/botui/ is the same text as the repository, and a test in CI proves it byte for byte.
Loading states, streaming indicators, agent status — the surfaces an agent product lives on. Install one with npx, and the code lands in your repo. No runtime dependency, nothing to keep current. Read it, change it, delete the name.
npx @botharness/botui add dot-matrixAlso works with shadcn, against the same registry:npx shadcn@latest add https://ui.botharness.ai/r/dot-matrix.json
dot / cell 198% dot / pitch 198% 100% = two dots touching touching at dot / cell = 1.00 pitch 43.5 × 43.5 px dot 86.1 px silhouette square dot square renderer css speed 1.00×
A traversal is one function of lattice position, so anything you can compute per cell you can animate. These two are not presets — they are `order` functions, written here and running below.
export const chevron: OrderFn = (col, row, cols, rows) => {
const mid = (rows - 1) / 2;
const arm = Math.abs(row - mid) / (mid || 1);
const along = col / (cols - 1 || 1);
// one band per step of the grid: any fewer and adjacent cells share a band and the arrow
// thickens, any more and there is no cell left to light
const bands = Math.max(cols, rows);
return Math.round(Math.abs(along - arm) * bands) / bands;
};
/**
* RINGS expanding from a dot the visitor chooses.
*
* Chebyshev distance — `max(|dc|, |dr|)` — rather than Euclidean, and that choice is the
* whole character: on an integer lattice the Chebyshev radius takes only a handful of
* distinct values, so every dot on a ring shares one phase and the field reads as discrete
* bands. Euclidean would give nearly every dot its own value and turn the same traversal
* into a smooth travelling wave. The concentric `ring` preset is this function with the
* origin pinned to the middle; here the origin is a parameter, which is the request the
* library's own table could not express.
*
* Normalised by the largest distance the lattice can hold, so the outermost dot leads at 1
* whatever the grid size — otherwise a 9×9 and a 3×3 would animate at different rates for
* the same nominal shape.
*/
export function ringsFrom(origin: { col: number; row: number }): OrderFn {
return (col, row, cols, rows) => {
const d = Math.max(Math.abs(col - origin.col), Math.abs(row - origin.row));
// The furthest cell in the lattice FROM THE ORIGIN, which is whichever corner is
// diagonally opposite it. Normalising by a fixed denominator — `cols`, or
// `max(cols, rows)` — is the tempting one-liner and it is wrong twice over: it makes
// the outermost dot's value depend on the GRID SIZE rather than on the origin, so a
// 9×9 and a 3×3 animate at different rates for the same nominal shape, and it leaves
// the last ring short of 1, so the wave never quite arrives.
const far = Math.max(origin.col, cols - 1 - origin.col, origin.row, rows - 1 - origin.row);
return far > 0 ? Math.min(1, d / far) : 0;
};
}
/**
* The cell a pointer is over, and the pixel position of a cell's centre.
*
* Both come from the ENGINE's own `layout()` numbers, and that is the whole point: the
* handle is drawn where this function says and the hit-test is taken against the same
* function, so "the handle is under the cursor" is true by construction rather than by two
* approximations agreeing.
*
* Two mistakes are written into the old version and are worth naming, because both produce
* a handle that drifts further out the further out you drag — which is exactly the symptom
* that sent this back:
*
* · the pitch was `box.width / cols`, which is not the pitch. The field's box is `size`
* px including one dot of overhang, so the centre-to-centre distance is
* `layout().pitchX` and the box/n is about 3% out before anything else is wrong;
* · and the centre-to-cell conversion multiplied by `(cols - 1) / 2` a second time, making
* the whole thing FOUR TIMES as sensitive as it should be. At the edge of a 9-wide field
* that is a twelve-cell error, clamped — so the handle sat at the rim while the cursor
* was halfway out, and dragging further changed nothing at all.
*
* The fixed version is the identity: a dot's centre is `dotPx/2 + col * pitchX` from the
* field's left edge, and a pointer at offset `x` from the field's centre is
* `x / pitchX` cells from the middle one.
*/
export interface FieldGeometry {
/** centre-to-centre distance in px, per axis */
pitchX: number;
pitchY: number;
/** one dot's width in px — the overhang at each end of the box */
dotPx: number;
}
/** where a cell's centre sits, relative to the field's box */
export function cellCentre(
geo: FieldGeometry,
cell: { col: number; row: number },
): { x: number; y: number } {
return {
x: geo.dotPx / 2 + cell.col * geo.pitchX,
y: geo.dotPx / 2 + cell.row * geo.pitchY,
};
}
/** the cell whose centre is nearest a point given as an offset from the field's centre */
export function nearestCell(
at: { x: number; y: number },
geo: FieldGeometry,
cols: number,
rows: number,
): { col: number; row: number } {
const col = Math.round((cols - 1) / 2 + at.x / geo.pitchX);
const row = Math.round((rows - 1) / 2 + at.y / geo.pitchY);
return {
// clamped, because a drag that leaves the field should park the origin at the rim
// rather than index off the lattice
col: Math.min(cols - 1, Math.max(0, col)),
row: Math.min(rows - 1, Math.max(0, row)),
};
}
Read from /registry.json — the same document the CLI installs from.
Loading the registry…
The registry ships the real source files. What lands in components/botui/ is the same text as the repository, and a test in CI proves it byte for byte.
There is no runtime dependency to upgrade and nothing that can break under you. When BotUI ships something better, you take the diff when you want to.
Lattice, traversal, brightness envelope, dot polygon, renderer. Each is a table you can extend without touching the layers above.