> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vendo.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Host components

> Register your own React components so generated screens render your real UI, what vendo sync captures into .vendo/components/, and what a capture cannot carry.

Register a component and a generated screen can name it. What you register is a closed contract, so a name a screen renders is a name your product really has.

## Register a catalog

One object, keyed by component name. `createVendo` reads it as `catalog`, and `<VendoProvider>` reads the same object as `components`.

```ts vendo.ts focus={9-14} theme={null}
import { createVendo } from "@vendoai/vendo/server";
import type { ComponentRegistry } from "@vendoai/vendo";
import { z } from "zod";
import { SpendingDonut } from "@/components/charts/spending-donut";

const registry = {
  MapleSpendingDonut: {
    component: SpendingDonut,
    description: "Category breakdown of spending. Use for where-did-my-money-go requests.",
    props: z.object({
      slices: z.array(z.object({ category: z.string(), amount: z.number() })),
      size: z.number().default(200),
    }),
    examples: ['{"slices":[{"category":"dining","amount":34218}],"size":200}'],
  },
} satisfies ComponentRegistry;

const vendo = createVendo({ catalog: registry });
```

* `description` is selection guidance. Say what the component shows and when to reach for it.
* `props` validates at render and derives the model-facing JSON Schema. One schema, never two.
* `examples` are JSON prop objects the model copies from, and they draw the console's preview.

<Warning>
  Copy prop names from the component's source, never from convention. If `SpendingDonut` takes `slices`, the schema says `slices`, not `data`.

  Prop invention is the single most observed agent failure. A schema is what makes it impossible.
</Warning>

## What a screen may name

<Frame caption="A name outside these two sets fails the save. It never renders as an empty box.">
  <svg viewBox="0 0 700 118" width="100%" role="img" aria-label="A generated screen may name the Kit's 51 components plus the components this host registered, and nothing else.">
    <rect x="1" y="1" width="330" height="72" rx="10" fill="currentColor" fillOpacity="0.04" stroke="currentColor" strokeOpacity="0.22" />

    <text x="166" y="27" fontSize="8" letterSpacing="1.2" textAnchor="middle" fill="currentColor" fillOpacity="0.55">SHIPPED WITH VENDO</text>
    <text x="166" y="48" fontSize="14" fontWeight="600" textAnchor="middle" fill="currentColor">the Kit</text>
    <text x="166" y="64" fontSize="11" textAnchor="middle" fill="currentColor" fillOpacity="0.55">51 components, themed by your tokens</text>
    <text x="350" y="43" fontSize="18" fontWeight="500" textAnchor="middle" fill="currentColor" fillOpacity="0.35">+</text>

    <rect x="369" y="1" width="330" height="72" rx="10" fill="#6c3bff" fillOpacity="0.08" stroke="#6c3bff" strokeOpacity="0.35" />

    <text x="534" y="27" fontSize="8" letterSpacing="1.2" textAnchor="middle" fill="#6c3bff" fillOpacity="0.8">SHIPPED WITH YOUR PRODUCT</text>
    <text x="534" y="48" fontSize="14" fontWeight="600" textAnchor="middle" fill="#6c3bff">your catalog</text>
    <text x="534" y="64" fontSize="11" textAnchor="middle" fill="#6c3bff" fillOpacity="0.75">the ones you registered, with their schemas</text>

    <line x1="350" y1="74" x2="350" y2="88" stroke="currentColor" strokeOpacity="0.25" strokeDasharray="3 3" />

    <rect x="212" y="88" width="276" height="29" rx="8" fill="none" stroke="currentColor" strokeOpacity="0.22" />

    <text x="350" y="107" fontSize="11" fontWeight="600" textAnchor="middle" fill="currentColor">everything a screen may render</text>
  </svg>
</Frame>

The Kit is one family, and every component in it reads your [theme tokens](/customize/theming).

| Family   | Components                                                                                                                                                  |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Layout   | `Stack` `Row` `Grid` `Surface` `Card` `Divider`                                                                                                             |
| Values   | `Text` `Money` `DateTime` `Percent` `Num` `EnumBadge` `Icon`                                                                                                |
| Data     | `DataTable` `CardList` `Stat` `Badge` `KeyValue` `Timeline` `Avatar` `CodeBlock`                                                                            |
| Charts   | `LineChart` `BarChart` `DonutChart` `Sparkline` `Progress`                                                                                                  |
| Forms    | `Input` `Select` `DatePicker` `Textarea` `Checkbox` `Switch` `Radio` `Slider` `SegmentedControl` `Combobox` `DateRange` `Button` `Link` `Form` `Disclaimer` |
| Feedback | `Tabs` `Callout` `Accordion` `Menu` `Tooltip` `EmptyState` `Steps`                                                                                          |
| Overlays | `Modal` `Sheet` `Toast`                                                                                                                                     |

`Accordion` is the one Kit name a generated screen may not reach for, because its panels arrive as element-valued props. `Tabs` takes its panels as children, so it stays available.

<Note>
  Two names people look for are deliberately absent. `DataTable` is the only table, and it sorts, filters, searches, paginates, resolves dot-path keys, and formats each cell.

  There is no `Skeleton` either. A loading placeholder is chrome the renderer paints while an app streams in.
</Note>

## What `vendo sync` captures

Two artifacts, both deterministic and byte-stable. Commit them.

<Frame caption="catalog.json is what your components are called. .vendo/components/ is what they are.">
  <svg viewBox="0 0 700 128" width="100%" role="img" aria-label="Your provider's components map feeds catalog.json for names and .vendo/components/ for source, split into per-component records and content-addressed modules.">
    <rect x="1" y="34" width="182" height="60" rx="10" fill="currentColor" fillOpacity="0.04" stroke="currentColor" strokeOpacity="0.22" />

    <text x="92" y="60" fontSize="8" letterSpacing="1.2" textAnchor="middle" fill="currentColor" fillOpacity="0.55">YOUR SOURCE</text>
    <text x="92" y="78" fontSize="11" fontWeight="600" textAnchor="middle" fill="currentColor">\<VendoProvider components></text>

    <line x1="183" y1="64" x2="222" y2="64" stroke="#6c3bff" strokeOpacity="0.4" strokeWidth="1.5" />

    <path d="M232 64 l-10 -5 v10 z" fill="#6c3bff" fillOpacity="0.6" />

    <text x="207" y="52" fontSize="9" textAnchor="middle" fill="#6c3bff">sync</text>

    <rect x="232" y="1" width="200" height="54" rx="10" fill="#6c3bff" fillOpacity="0.08" stroke="#6c3bff" strokeOpacity="0.35" />

    <text x="332" y="24" fontSize="11" fontWeight="600" textAnchor="middle" fill="#6c3bff">.vendo/catalog.json</text>
    <text x="332" y="42" fontSize="10" textAnchor="middle" fill="#6c3bff" fillOpacity="0.75">names, prop schemas, examples</text>

    <rect x="232" y="73" width="200" height="54" rx="10" fill="#6c3bff" fillOpacity="0.08" stroke="#6c3bff" strokeOpacity="0.35" />

    <text x="332" y="96" fontSize="11" fontWeight="600" textAnchor="middle" fill="#6c3bff">.vendo/components/</text>
    <text x="332" y="114" fontSize="10" textAnchor="middle" fill="#6c3bff" fillOpacity="0.75">the real source, content addressed</text>

    <line x1="432" y1="100" x2="471" y2="100" stroke="currentColor" strokeOpacity="0.3" />

    <path d="M481 100 l-10 -5 v10 z" fill="currentColor" fillOpacity="0.4" />

    <rect x="481" y="66" width="218" height="26" rx="7" fill="none" stroke="currentColor" strokeOpacity="0.22" />

    <text x="590" y="83" fontSize="10" textAnchor="middle" fill="currentColor" fillOpacity="0.8">\<Name>.json, refs only</text>

    <rect x="481" y="98" width="218" height="26" rx="7" fill="none" stroke="currentColor" strokeOpacity="0.22" />

    <text x="590" y="115" fontSize="10" textAnchor="middle" fill="currentColor" fillOpacity="0.8">modules/\<hex>.json, the bytes</text>
  </svg>
</Frame>

`.vendo/catalog.json` records what your components are called: the module that exports each one, its JSON Schema props, its description, and its examples. Rescans preserve description and example copy you edited by hand.

`.vendo/components/<Name>.json` records what they are. It holds no source of its own, only the module that declares the component, the binding to render, a content hash, and references.

Every byte lives beside it in `.vendo/components/modules/<hex>.json` as `{ source, imports? }`, keyed by the sha-256 of its own content. A `format-currency.ts` that ten components import is stored once and referenced ten times.

### Where the walk stops

The import walk follows your own code to closure, with no depth limit. It stops at exactly two lines.

* **The package boundary.** Anything resolving into `node_modules` is never captured, because it is not your code.
* **A byte budget.** 256 KB of source per component, counting the whole closure.

Three packages are bundled into the sandbox itself, because they blocked almost everything and are tiny.

| Bundled          | What you get                            |
| ---------------- | --------------------------------------- |
| `clsx`           | The real package, pinned `2.1.1`        |
| `tailwind-merge` | The real package, pinned `3.6.0`        |
| `zod`            | A zod-shaped shim for declaring schemas |

<Note>
  **The bundled `zod` is a shim, not zod.** It resolves the declaration surface so your module loads, which is all a registry's `props:` schema needs.

  It does not validate. `.parse()` and `.safeParse()` throw a named `ZodShimError` rather than returning a plausible wrong value.
</Note>

### When a component cannot be captured

Sync never goes quiet. The record on disk carries a machine-readable `skipped.reason` plus a sentence you can read.

| `skipped.reason`          | Meaning                                                                                        |
| ------------------------- | ---------------------------------------------------------------------------------------------- |
| `too-large`               | The closure is over the 256 KB budget, and the record names the biggest module                 |
| `unsupported-imports`     | The closure imports specifiers the sandbox cannot resolve, and the record names each one       |
| `no-default-export`       | Registered as a module's default export, but the module has none                               |
| `default-export-conflict` | The module default-exports something else, so the registered binding cannot become the default |
| `in-package`              | Declared inside `node_modules`                                                                 |
| `no-named-declaration`    | The registered value has no name to re-export                                                  |

A component that could not be read at all this run is a different case. Sync leaves the previous capture exactly as it was and moves on.

## How a preview draws

A preview has no data plane, so every query resolves to an empty list. Sync solves that from things you have already written, resolving a seed down three rungs.

<Steps>
  <Step title="Your examples">
    The first `examples` string that parses to a JSON object wins. A human's example is real product data, and it reads better than anything Vendo could invent.
  </Step>

  <Step title="Generated from your props schema">
    Values synthesized from the schema: typed correctly, respecting enums, `min`, `max`, string formats, and optionality. Plausible rather than pretty.
  </Step>

  <Step title="An honest label">
    With neither, the capture records `noSampleProps` with a reason. The component still renders in your product, and previews as a labeled placeholder.
  </Step>
</Steps>

Generation is seeded from the component's name and each property path, so the same schema always produces the same values. Your committed `.vendo/components/` never churns.

Examples and generated values both travel with the capture. Treat them as you would any other value that crosses the wire, and use realistic but non-sensitive data.

## Sending the corpus to Cloud

Sync offers to push the corpus so the console can render your components instead of grey placeholders. It asks once per project and commits the answer to `.vendo/cloud.json`.

```json .vendo/cloud.json theme={null}
{ "pushComponents": true }
```

In CI, pass `--push-components` or `--no-push-components`. A non-interactive run with no saved answer and no flag pushes nothing and says so.

| Crosses the wire                                 | Never crosses                      |
| ------------------------------------------------ | ---------------------------------- |
| The source of every component you register       | Package code                       |
| The source of every module in its import closure | Anything outside your project root |
| Your app-root stylesheets                        | Environment variables              |
| The `examples` you declared                      | Live data                          |

The push is cheap by construction. Component records carry references rather than source, so listing them is the hash manifest, and one keys-only call answers which module bodies the console already has.

## Where to go next

<Columns cols={2}>
  <Card title="Import & fork" href="/generated/import-and-fork">
    Let a user fork one of your components and make it theirs.
  </Card>

  <Card title="In-client venue & approvals" href="/generated/in-client-venue">
    Approve a version and it runs as real code in your page.
  </Card>
</Columns>
