registry.json
The catalog file annotated field by field, with the opsinjs conventions layered on top of the shadcn specification.
Overview
registry.json is the catalog: one file that lists every item a registry
distributes. It is what the CLI reads first, what the MCP server browses, and
what a search command indexes.
The authoritative schema is shadcn's, published at
https://ui.shadcn.com/schema/registry.json, and opsinjs implements it without
extending it. opsinjs sometimes needs a field the schema has no column for:
status, implemented, category, aliases, and the doctrine pages that govern a
component. Those sit under meta, which the specification reserves for exactly
this, and are republished in a flatter shape at /r/index.json for a consumer
that wants them without having to know where inside meta they live. Nothing
opsinjs emits sits outside the schema: an item that fails somebody else's
validator is a worse outcome than a second request.
The file
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "opsinjs",
"homepage": "https://opsinjs.pensievelabs.org",
"items": [
{
"name": "result-card",
"type": "registry:ui",
"title": "ResultCard",
"description": "One test result, showing the number, what it is compared against, and what it means.",
"categories": ["health-data-display"],
"registryDependencies": ["@opsinjs/status-pill", "@opsinjs/value", "…"],
"files": [
{ "path": "registry/bases/base/result-card.tsx", "type": "registry:ui", "target": "@ui/result-card.tsx" },
{ "path": "lib/opsinjs.ts", "type": "registry:lib", "target": "@lib/opsinjs.ts" },
{ "path": "lib/status.ts", "type": "registry:lib", "target": "@lib/status.ts" }
],
"docs": "ResultCard is documented at https://opsinjs.pensievelabs.org/components/result-card",
"meta": { "opsinjs": { "implemented": true, "status": "shipped", "…": "…" } }
}
]
}One row, abridged. Note what is present and what is not: paths and targets but no
content, and the opsinjs fields tucked inside meta rather than beside the
schema's own.
Field by field
| Field | Required | What it is |
|---|---|---|
$schema | No, in practice yes | Points at the schema. Editors validate against it, and a typo becomes an error instead of a mystery |
name | Yes | The registry's own name, not a namespace. The namespace is chosen by the consumer in components.json |
homepage | Yes | Where a human goes to understand what this is. Surfaced by search and by the MCP server |
items | Yes | The array. Order is not significant |
items[].name | Yes | The item id. Kebab-case, and in opsinjs it is exactly the catalogue id and the docs URL segment |
items[].type | Yes | What kind of thing it is, one of the types listed below |
items[].title | Yes | Human-readable. PascalCase in opsinjs, the same string as the exported component. That is ResultCard, not "Result card" |
items[].description | Yes | One sentence. In opsinjs this is the plain-English definition written for the patient, not the engineer |
items[].files | For anything with code | Path, type and target per file. No content in the catalog, as the rule below explains. Absent only on a planned entry, which carries no source to install, and there is none today |
items[].dependencies | No | npm packages the item needs |
items[].registryDependencies | No | Other items it needs, by name or by URL. opsinjs names them namespaced, @opsinjs/status-pill |
items[].categories | No | Free-form grouping used by search |
items[].docs | No | The note the CLI prints after install |
items[].meta | No | The schema's escape hatch, and where every opsinjs-specific field lives. Those fields are implemented, status, category, aliases and governedBy |
The catalog carries no file contents
files[].content must not appear in registry.json. The catalog is an index; it
is fetched constantly, by every tool, for every command, and inlining source
turns a small file into a large one for no benefit. Contents belong in the
per-item response at /r/<name>.json, which is fetched once, only when
something is actually being installed.
This is also a requirement for listing in the public registry directory, so it is worth getting right even if you never intend to be listed.
Item types
The types opsinjs uses:
| Type | Used for |
|---|---|
registry:ui | Every catalogue item, and the component file inside it. All sixty rows are this |
registry:lib | Shared non-component code. These are lib/opsinjs.ts and lib/status.ts, which ride along inside each component item. A file type, never an item type |
registry:theme | A theme: cssVars and no files, served from /r/themes/<preset>.json |
Three, and no more. The specification also defines registry:component,
registry:hook, registry:block and others; opsinjs emits none of them today,
so a consumer switching on type only ever sees the three above. registry:block
is the one worth naming, because it is where composed multi-file screens would
go and the catalogue has no screens in it. A tool that branches on it will
branch on a case that never arrives. Consult shadcn's schema rather than this
table if you are authoring a registry of your own: this list is what opsinjs
emits, not the full vocabulary.
opsinjs conventions on top
Four, all of them enforced by scripts/assert-ia.mts rather than by review.
One id everywhere. A component's registry name, its catalogue id, its
documentation URL segment and its file name are the same kebab-case string.
There is no mapping table, because a mapping table is a place for two things to
drift apart.
Descriptions are plain English. The description field is what appears in a
search result, in an MCP listing, and as a card subtitle. It is written for
somebody with no clinical training, in the same voice as the product itself.
Write "Shows a value against a reference range" rather than "RangeBar component
with configurable thresholds".
No field outside the schema. Status, implemented, category, aliases and the
accessibility-review date are opsinjs's, not shadcn's, so they go under meta,
which is the place the specification set aside for them, and are republished flat
at /r/index.json for a consumer that would rather not walk into meta to find
them. Nothing is invented at the top level, which is what keeps an opsinjs item
valid against a validator that has never heard of opsinjs.
Every id resolves. All sixty implemented components appear somewhere
addressable, and so would a planned id. An agent asking about a component that
has not been built gets a definite "planned, not implemented" answer and never a
404. That is the failure the whole design exists to eliminate.
Generated, not hand-written
registry.json is emitted by scripts/build-registry.mts from
registry/catalogue.ts, and pnpm run check:generated fails the build if the
committed output no longer matches its source. Hand-editing the JSON is caught
and reverted.
The catalogue is therefore the file to change. It is also the sole owner of the alias namespace, which is what makes global uniqueness achievable while many people write pages in parallel.
/r/result-card.jsonLoading…
Verify it worked
It validates
Any JSON Schema validator against the $schema URL. Editors with schema support
do this as you type.
It is flat and content-free
Search the file for "content". There should be no matches.
Names match the site
Every items[].name should correspond to a documentation URL segment. A name
that does not resolve is caught by assert-ia, and if you are building your own
registry it is worth asserting the same thing.
Troubleshooting
The CLI reports an invalid registry. Usually files[].content present in the
catalog, or an item missing type.
An item resolves but installs nothing. That would describe a planned
entry, which carries no files at all, and there is none today: every opsinjs id
installs real code. Read meta.opsinjs.implemented to
confirm. If a built item, or one in your own registry, installs nothing, check
that files[].path points at a file that exists relative to the registry root.
Search finds nothing. search matches on name, title and description.
An item with a terse description is hard to find; that is a content bug.
check:generated fails on registry.json. It was hand-edited. Change
registry/catalogue.ts and regenerate.
Next
- registry-item.json covers one distributed item, in full.
- Machine-readable schemas covers the
companion
/r/index.json.
Namespaces
Register @opsinjs in your project, compose it with other registries, and understand what a namespace does and does not guarantee.
registry-item.json
One distributed item annotated field by field, covering files, targets, dependencies and CSS variables, and the fields opsinjs deliberately leaves empty.