opsinjs
HandbookRegistry & distribution

registry-item.json

One distributed item annotated field by field, covering files, targets, dependencies and CSS variables, and the fields opsinjs deliberately leaves empty.

Overview

A registry item is what add actually installs. Where the catalog lists names, an item carries everything needed to write files into a project: the source, the paths, the npm dependencies, the other items it needs, and any CSS variables it contributes.

opsinjs serves one at /r/<name>.json, and one per style at /r/styles/<style>/<name>.json. The authoritative schema is shadcn's, at https://ui.shadcn.com/schema/registry-item.json.

All sixty items carry real files, each with content: the component source plus the shared substrate, lib/opsinjs.ts and lib/status.ts. Every item that resolves is a built component. A planned id would omit files entirely rather than carry an empty array, and would report implemented: false. The field to read is meta.opsinjs.implemented, on the item itself or flattened into /r/index.json.

The file

/r/result-card.json
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "result-card",
  "type": "registry:ui",
  "title": "ResultCard",
  "description": "One test result, showing the number, what it is compared against, and what it means.",
  "author": "opsinjs",
  "categories": ["health-data-display"],
  "docs": "ResultCard is documented at https://opsinjs.pensievelabs.org/components/result-card",
  "meta": {
    "opsinjs": {
      "implemented": true,
      "status": "shipped",
      "category": "health-data-display",
      "base": "base",
      "style": "base-lyra",
      "since": "unreleased",
      "owner": "clinical",
      "healthCategory": "labs"
    }
  },
  "registryDependencies": [
    "@opsinjs/status-pill",
    "@opsinjs/value",
    "@opsinjs/range-bar",
    "@opsinjs/relative-time",
    "@opsinjs/button"
  ],
  "files": [
    { "path": "registry/bases/base/result-card.tsx", "type": "registry:ui", "target": "@ui/result-card.tsx", "content": "…" },
    { "path": "lib/opsinjs.ts", "type": "registry:lib", "target": "@lib/opsinjs.ts", "content": "…" },
    { "path": "lib/status.ts", "type": "registry:lib", "target": "@lib/status.ts", "content": "…" }
  ]
}

meta.opsinjs is abridged above: the served object also carries aliases, governedBy, usedIn and the three URLs docs, markdown and catalogue, so an item read on its own can find its own documentation. A planned id would serve the same envelope with implemented: false, a why sentence, and no files and no registryDependencies at all. No item does today.

Field by field

FieldWhat it isopsinjs practice
nameThe item idIdentical to the catalogue id and the docs URL segment
typeWhat kind of thing this isSee the table in registry.json
titleHuman-readable namePascalCase, the same string as the catalogue title and the exported component. That is ResultCard, not "Result card"
descriptionOne sentencePlain English, written for the patient not the engineer
authorAttributionThe literal string opsinjs on every item; the registry's homepage carries the URL
dependenciesnpm packagesKept as small as possible, and omitted rather than sent empty; a health component that pulls a charting library is a design failure
devDependenciesnpm dev packagesUnused
registryDependenciesOther items, by name or full URLComposition is explicit, and namespaced. result-card names @opsinjs/status-pill rather than inlining it
filesThe code, with path, type, target and contentPresent on all sixty items; a planned id would omit them entirely
cssVarstheme, light, dark objectsUsed by themes; components contribute none
cssRaw CSS to merge into the stylesheetAvoided, for the reasons given below
metaFree-formWhere opsinjs puts implemented, status, category, since and the page URLs
docsA note shown after installThe title and docs URL for an implemented item; the not-implemented explanation for the rest
categoriesGrouping for searchMirrors the catalogue category

files

Each entry has a path, a type and a target. A served item also has content.

{
  "path": "registry/bases/base/result-card.tsx",
  "type": "registry:ui",
  "target": "@ui/result-card.tsx",
  "content": "…"
}

target is the field worth understanding. Without it, the CLI decides the destination from your components.json aliases alone. With it, the item states where the file goes. A target that begins with an alias placeholder, as every opsinjs target does, still resolves through your components.json rather than around it. @ui/result-card.tsx means "your components directory", not a path opsinjs chose. A literal path would be the other use: a file that is not a component, such as a config file at the project root.

registryDependencies

Names resolve within the same registry; full URLs resolve anywhere. A cross-registry dependency must be a URL or a namespaced name the consumer has configured. A bare name will resolve against the wrong registry, silently.

opsinjs composes rather than inlines. result-card depends on @opsinjs/status-pill, @opsinjs/value, @opsinjs/range-bar, @opsinjs/relative-time and @opsinjs/button instead of containing its own copy of any of them. This means installing one component installs several files, which surprises people, and it is the correct trade: two copies of a status renderer will eventually disagree about what urgent looks like. The cost is the namespace: because these names are namespaced, the CLI needs an @opsinjs entry in your components.json to follow them, even when you asked for result-card by its full URL.

cssVars and css

cssVars is structured and mergeable: three objects, theme, light and dark, whose keys are variable names without the leading dashes. This is how presets are distributed, and a theme item is nothing but this.

css is raw CSS appended to your stylesheet. opsinjs avoids it. The order of app/globals.css is load-bearing, as Tailwind v4 explains, and an item that appends raw rules lands at whatever position the CLI chooses, which is not a position anybody reasoned about. Structured cssVars merge predictably; raw css accumulates.

meta and docs

meta is the spec's escape hatch and opsinjs uses it for the fields the schema has no room for. It is duplicated in /r/index.json in a flatter shape, because a consumer that only wants status should not have to know where in meta it lives.

docs is a string the CLI prints after install. On an implemented item opsinjs uses it for the component's name and its documentation URL. On a reserved name it carries the whole explanation: that the id resolves, that nothing sits behind it, and where to propose it. It does this so that the fact reaches somebody who never opens the documentation.

Generated, not hand-written

Items are emitted by scripts/build-registry.mts from registry/catalogue.ts and the source files under registry/. pnpm run check:generated fails on a diff. Change the catalogue, not the JSON.

Verify it worked

The item validates and resolves

curl -s https://opsinjs.pensievelabs.org/r/result-card.json | head -20

Its dependencies exist

Every name in registryDependencies should itself resolve, which for a namespaced name means the consuming project has an @opsinjs entry. A dangling dependency fails at install time, in somebody else's project, which is the worst place to discover it.

Its status is visible

meta.opsinjs.implemented and meta.opsinjs.status should be present and should agree with /r/index.json and with the documentation page. Three copies of one fact, asserted to agree.

Troubleshooting

Files land in the wrong directory. Either the item declares a target you did not expect, or your components.json aliases point somewhere else.

add pulls in more than I asked for. registryDependencies. Read them before installing; composition is explicit for exactly this reason.

CSS variables were added but nothing changed. They were merged into the stylesheet named by tailwind.css in components.json, which may not be the one your app imports.

An item installs into a project with no components.json. Only if every file declares a target. Otherwise the CLI has nowhere to put anything.

Next

On this page