opsinjs
HandbookRegistry & distribution

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

registry.json
{
  "$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

FieldRequiredWhat it is
$schemaNo, in practice yesPoints at the schema. Editors validate against it, and a typo becomes an error instead of a mystery
nameYesThe registry's own name, not a namespace. The namespace is chosen by the consumer in components.json
homepageYesWhere a human goes to understand what this is. Surfaced by search and by the MCP server
itemsYesThe array. Order is not significant
items[].nameYesThe item id. Kebab-case, and in opsinjs it is exactly the catalogue id and the docs URL segment
items[].typeYesWhat kind of thing it is, one of the types listed below
items[].titleYesHuman-readable. PascalCase in opsinjs, the same string as the exported component. That is ResultCard, not "Result card"
items[].descriptionYesOne sentence. In opsinjs this is the plain-English definition written for the patient, not the engineer
items[].filesFor anything with codePath, 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[].dependenciesNonpm packages the item needs
items[].registryDependenciesNoOther items it needs, by name or by URL. opsinjs names them namespaced, @opsinjs/status-pill
items[].categoriesNoFree-form grouping used by search
items[].docsNoThe note the CLI prints after install
items[].metaNoThe 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:

TypeUsed for
registry:uiEvery catalogue item, and the component file inside it. All sixty rows are this
registry:libShared 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:themeA 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.json
Loading…

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

On this page