---
title: "registry.json"
description: "The catalog file annotated field by field, with the opsinjs conventions layered on top of the shadcn specification."
url: "https://opsinjs.pensievelabs.org/registry/registry-json"
source: "https://opsinjs.pensievelabs.org/registry/registry-json.md"
section: "Registry & distribution"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["catalog", "registry schema", "registry root"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="guide" />

## Overview [#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 [#the-file]

```json title="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 [#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. &#x2A;*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 [#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 [#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 [#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 [#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.

<RegistryItem name="result-card" />

## Verify it worked [#verify-it-worked]

<Steps>
  ### It validates [#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 [#it-is-flat-and-content-free]

  Search the file for `"content"`. There should be no matches.

  ### Names match the site [#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.
</Steps>

## Troubleshooting [#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 [#next]

* [registry-item.json](./registry-item-json.mdx) covers one distributed item, in
  full.
* [Machine-readable schemas](../agents/machine-readable-schemas.mdx) covers the
  companion `/r/index.json`.
