Registry
Not a gallery. A manifest.
Every component is published in the shadcn/ui registry-item schema, with its source inlined rather than linked. A CLI, an MCP server, an agent or a 20-line script can list the catalog, read a component's exact capabilities, and install it without anyone opening a browser.
Try it live
This panel is not a mock. It fetches /r/index.json from your browser and then each item as you click it, the same two requests an agent would make.
Loading index…
Endpoints
Reads are open and cacheable. The only endpoint that touches your workspace is the install one, and its plan mode deliberately requires no credentials at all.
| Method | Path | Returns | Auth |
|---|---|---|---|
| GET | /r/index.json | The whole catalog in the shadcn registry.json envelope | none |
| GET | /r/{name}.json | One item, with its HTML / recipe JSON / manual steps inlined | none |
| GET | /r/schema.json | JSON Schema for every widget's configuration parameters | none |
| GET | /embed/{slug} | The configured widget as a framable HTML document | none |
| GET | /api/widget/{slug} | The same HTML with config baked in, as a file download | none |
| GET | /widgets/{slug}.html | Raw source, exactly as it is committed | none |
| POST | /api/install | Plan or execute an install. mode: plan needs no credentials | token for writes |
Consuming it
Three ways in, from least to most code.
Point an agent at the registry
Use the official Notion Arena MCP server or Agent Skill to give Claude Code, Cursor, or Claude Desktop tools to browse, configure, and install components directly.
# Add MCP server to Claude Code
claude mcp add --scope project notion-arena -- npx -y notion-arena-mcp
# Or run standalone via npx
npx -y notion-arena-mcp@0.2.2
# Install Agent Skill (cross-agent via skills.sh)
npx skills add SynergyFlowLabs/notion-arena --skill notion-arena{
"mcpServers": {
"notion-arena": {
"command": "npx",
"args": ["-y", "notion-arena-mcp@0.2.2"],
"env": {
"NOTION_ARENA_SITE_URL": "https://notionarena.vercel.app"
}
}
}
}Two fetches and a file write
The index gives you names and capabilities. The item gives you the bytes. That is the whole protocol: there is no SDK and no auth for reads.
// Node 20+: no dependencies
const registry = "https://notionarena.vercel.app/r";
const { items } = await (await fetch(`${registry}/index.json`)).json();
// pick the interactive, offline-safe widgets
const candidates = items.filter(
(i) => i.meta.kind === "widget" && i.meta.capabilities.offlineSafe
);
for (const item of candidates) {
const full = await (await fetch(`${registry}/${item.name}.json`)).json();
const html = full.files.find((f) => f.path.endsWith(".html")).content;
// write it somewhere your installer can read
await Bun.write(`./notion/widgets/${item.name}.html`, html);
}Or let the server do the Notion calls
Plan mode returns the exact request sequence with bodies. Switch to api mode with a token and a parent page id and it executes them, including the two-pass data-source resolution relations require.
# 1 · see the plan: no credentials, no side effects
curl -s -X POST https://notionarena.vercel.app/api/install \
-H 'content-type: application/json' \
-d '{"component":"crm-pipeline","mode":"plan"}' | jq '.plan.calls[] | .method + " " + .path'
# 2 · run it
curl -s -X POST https://notionarena.vercel.app/api/install \
-H 'content-type: application/json' \
-d '{
"component": "crm-pipeline",
"mode": "api",
"token": "'"$NOTION_TOKEN"'",
"parentPageId": "'"$PAGE_ID"'"
}'# a widget, installed as a native HTML block
curl -s -X POST https://notionarena.vercel.app/api/install \
-H 'content-type: application/json' \
-d '{
"component": "pomodoro-timer",
"mode": "html-block",
"token": "'"$NOTION_TOKEN"'",
"parentPageId": "'"$PAGE_ID"'",
"config": { "work": 50, "short": 10, "accent": "#0f7b6c" }
}'The item schema
The envelope is shadcn's, because tooling already understands it. The meta object is ours, and it carries the two things a consumer actually needs to decide what to do: which channels apply, and what the component is capable of.
namestringThe slug. Stable, lowercase, kebab-case: the address you install by.typeregistry:component | registry:blockcomponent for widgets (a file you place), block for recipes (a structure you create).title / descriptionstringHuman-facing name and the one-line pitch shown in the catalog.categoriesstring[]Which of the eight catalog categories it belongs to.tagsstring[]kind, tier and the component's own tags. Filterable, and what search matches against.meta.kindwidget | nativeIt tells you which delivery channels apply.meta.tierfree | proWhether it is in the free set.meta.version / updatedAtsemver / dateFor deciding whether to re-install. Downloaded HTML blocks are frozen; embed URLs are not.meta.deliverystring[]The channels this component actually supports, never guessed from kind.meta.capabilitiesobjectinteractive, offlineSafe, persistsLocally, writesToNotion, needsNetwork. Booleans, not adjectives.meta.paramsWidgetParam[]Widgets only: every configuration key with type, bounds, options, default and help text.meta.embedUrl / downloadUrlstringWidgets only: the two URLs a consumer needs, already absolute.meta.recipe / installUrlstringRecipes only: the recipe id and the endpoint that installs it.files[]object[]path, type, target and (on /r/{name}.json) content. The HTML is inlined, not linked.Versioning
Items carry semver in meta.version. Widget source files are frozen per version at /widgets/{slug}.html; embed URLs always serve the latest patch. If you are shipping a template to a customer, download the file: that is the whole point of the second channel.
Stability
Slugs never change once published. A slug that is retired keeps resolving with a meta.deprecated flag rather than 404ing, so a manifest pinned in someone's config does not silently break.
Self-hosting
Widgets are single files with no dependencies. Fork the repo, serve public/widgets/ from anywhere, and point your registry base at your own host. Nothing phones home: there is nothing to phone.
Caching
Registry JSON is cached 5 minutes at the edge with stale-while-revalidate. Widget HTML is cached 60 seconds, so a configuration change is visible almost immediately while repeat loads stay cheap.
Everything published
203 items. Click a name for the human page, or the code for the manifest.