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
{
"$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
| Field | What it is | opsinjs practice |
|---|---|---|
name | The item id | Identical to the catalogue id and the docs URL segment |
type | What kind of thing this is | See the table in registry.json |
title | Human-readable name | PascalCase, the same string as the catalogue title and the exported component. That is ResultCard, not "Result card" |
description | One sentence | Plain English, written for the patient not the engineer |
author | Attribution | The literal string opsinjs on every item; the registry's homepage carries the URL |
dependencies | npm packages | Kept as small as possible, and omitted rather than sent empty; a health component that pulls a charting library is a design failure |
devDependencies | npm dev packages | Unused |
registryDependencies | Other items, by name or full URL | Composition is explicit, and namespaced. result-card names @opsinjs/status-pill rather than inlining it |
files | The code, with path, type, target and content | Present on all sixty items; a planned id would omit them entirely |
cssVars | theme, light, dark objects | Used by themes; components contribute none |
css | Raw CSS to merge into the stylesheet | Avoided, for the reasons given below |
meta | Free-form | Where opsinjs puts implemented, status, category, since and the page URLs |
docs | A note shown after install | The title and docs URL for an implemented item; the not-implemented explanation for the rest |
categories | Grouping for search | Mirrors 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 -20Its 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
- Upgrades and diffs covers taking a newer item over code you have edited.
- Presets covers the theme-only item in practice.