---
title: "registry-item.json"
description: "One distributed item annotated field by field, covering files, targets, dependencies and CSS variables, and the fields opsinjs deliberately leaves empty."
url: "https://opsinjs.pensievelabs.org/registry/registry-item-json"
source: "https://opsinjs.pensievelabs.org/registry/registry-item-json.md"
section: "Registry & distribution"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["registry item", "item schema", "item json", "cssVars"]
---

> 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]

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`.

<Callout>
  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`.
</Callout>

## The file [#the-file]

```json title="/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 [#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](./registry-json.mdx)                                                                               |
| `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` [#files]

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

```json
{
  "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` [#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-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](../theming/presets.mdx) 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](../theming/tailwind-v4.mdx)
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-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 [#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 [#verify-it-worked]

<Steps>
  ### The item validates and resolves [#the-item-validates-and-resolves]

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

  ### Its dependencies exist [#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 [#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.
</Steps>

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

* [Upgrades and diffs](./upgrades-and-diffs.mdx) covers taking a newer item over
  code you have edited.
* [Presets](../theming/presets.mdx) covers the theme-only item in practice.
