---
title: "Registry & distribution"
description: "opsinjs is distributed as a shadcn-spec registry rather than as an npm package, and this is what that decision buys and what it costs."
url: "https://opsinjs.pensievelabs.org/registry"
source: "https://opsinjs.pensievelabs.org/registry.md"
section: "Registry & distribution"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["distribution", "install", "copy not dependency", "shadcn registry"]
---

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

opsinjs is not an npm package you depend on. It is a registry you copy from. You
run a command, source files land in your repository, and from that moment you
own them: you can read them, edit them, delete them, and nobody can change them
underneath you.

That model comes from shadcn, and opsinjs implements shadcn's registry
specification rather than inventing one. The decision is recorded as
[ADR 0002. Distribute as a shadcn registry](../project/decisions/0002-shadcn-registry-distribution.mdx).
This page explains what the trade actually is, because it is a real trade with a
real cost and the cost is usually described less clearly than the benefit.

<Callout>
  All sixty ids carry real `files` with `content`. That content is the component
  itself plus the shared substrate, `lib/opsinjs.ts` and `lib/status.ts`. Every id
  resolves to installable source and each carries
  `meta.opsinjs.implemented: true`. A `planned` id would omit `files` and report
  `implemented: false`, and that flag stays visible in `/r/index.json` either way.
</Callout>

## What you get [#what-you-get]

**Source you can read.** A health component's behaviour is a safety-relevant
detail. When a `RangeBar` decides how to render a value with an unknown
reference range, you should be able to read that decision rather than infer it
from a prop table. Copied source makes that the default rather than an
archaeology exercise through a bundle.

**No version conflicts.** Copied code has no peer dependency, cannot be
duplicated in a lockfile at two versions, and cannot break because a transitive
dependency of a dependency changed. In a long-lived clinical product this
matters more than it does elsewhere; these codebases are maintained for years by
people who did not write them.

**Modification without forking.** Changing a component is editing a file. There
is no wrapper, no `styled(...)`, no shadow DOM, no `!important` cascade fight.

**A machine-readable inventory for free.** Because the registry is a spec other
tools already implement, the shadcn CLI and its MCP server work against opsinjs
with no adapter. That is most of how
[Agents & automation](../agents/index.mdx) gets to be short.

## What it costs [#what-it-costs]

Three costs, stated plainly.

**You do not get updates automatically.** Copied code is your code. An
improvement here does not reach you until you deliberately take it. That is the
entire point and it is also the largest ongoing cost, which is why
[Upgrades and diffs](./upgrades-and-diffs.mdx) is not optional reading.

**You inherit the maintenance.** A security fix in a copied file is your job.
There is no `npm audit` line that tells you a component you copied has a
problem.

**Drift is invisible without help.** Two teams in one company copy the same
component in different months and now have two different components with one
name. Nothing in the file makes that visible, so what does is your own record of
what you took and when: commit the files `shadcn add` writes, in their own
commit, so a later `--diff` has something to compare against.

## The shape of the registry [#the-shape-of-the-registry]

Three kinds of thing are distributed, addressable at stable paths:

| Path                            | What                                                                                                         |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `/r/registry.json`              | The catalog, which lists every item and carries no file contents. What the CLI and the MCP server read first |
| `/r/<name>.json`                | One item at the default base and style, with file contents inlined                                           |
| `/r/styles/<style>/<name>.json` | The same item at an explicit style, for the base × style matrix                                              |
| `/r/themes/<preset>.json`       | A theme-only item: `cssVars` and no files                                                                    |
| `/r/index.json`                 | The opsinjs view of status, category and aliases, which the shadcn spec has no field for                     |

The last row is worth noticing. `registry.json` is somebody else's schema and
opsinjs does not extend it with private fields, because an item that fails
validation elsewhere is worse than a second file. `index.json` carries what the
spec does not.

## Install from opsinjs [#install-from-opsinjs]

Two forms. The full URL needs no configuration, for an item that depends on
nothing else here:

<CodeBlockTabs defaultValue="npm" groupId="package-manager">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx shadcn@latest add https://opsinjs.pensievelabs.org/r/status-pill.json
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest add https://opsinjs.pensievelabs.org/r/status-pill.json
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx shadcn@latest add https://opsinjs.pensievelabs.org/r/status-pill.json
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x shadcn@latest add https://opsinjs.pensievelabs.org/r/status-pill.json
    ```
  </CodeBlockTab>
</CodeBlockTabs>

The namespaced form is shorter and requires configuration first:

<CodeBlockTabs defaultValue="npm" groupId="package-manager">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx shadcn@latest add @opsinjs/status-pill
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest add @opsinjs/status-pill
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx shadcn@latest add @opsinjs/status-pill
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x shadcn@latest add @opsinjs/status-pill
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Add the namespace to your `components.json` to use the second form, and to
install any component that builds on another, whichever form you name it by:

```json title="components.json"
{
  "registries": {
    "@opsinjs": "https://opsinjs.pensievelabs.org/r/{name}.json"
  }
}
```

<Callout>
  Twenty-one items list their siblings in `registryDependencies`, and they list
  them namespaced as `@opsinjs/status-pill` rather than a URL, so the CLI needs the
  `registries` block above in order to follow them, even when you named the item
  by its full URL. Those twenty-one are alert-banner, card, care-card,
  consent-sheet, dialog, disclaimer-note, dose-tracker, empty-state, icon-button,
  link, log-sheet, metric-tile, range-bar, reading-input, result-card, score-dial,
  sheet, source-citation, tab-bar, timeline-entry and trend-sparkline. Configuring
  the namespace once is less work than remembering which list a component is on.
</Callout>

[Namespaces](./namespaces.mdx) covers composing several registries, private ones
with authentication, and why `@opsinjs` is spelled the way it is.

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

<Steps>
  ### The catalog resolves [#the-catalog-resolves]

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

  You should get a `name`, a `homepage` and an `items` array.

  ### The CLI can see it [#the-cli-can-see-it]

  <CodeBlockTabs defaultValue="npm" groupId="package-manager">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="npm">
        npm
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="pnpm">
        pnpm
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="yarn">
        yarn
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="bun">
        bun
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="npm">
      ```bash
      npx shadcn@latest search @opsinjs
      ```
    </CodeBlockTab>

    <CodeBlockTab value="pnpm">
      ```bash
      pnpm dlx shadcn@latest search @opsinjs
      ```
    </CodeBlockTab>

    <CodeBlockTab value="yarn">
      ```bash
      yarn dlx shadcn@latest search @opsinjs
      ```
    </CodeBlockTab>

    <CodeBlockTab value="bun">
      ```bash
      bun x shadcn@latest search @opsinjs
      ```
    </CodeBlockTab>
  </CodeBlockTabs>

  Sixty items, every one of them implemented. `implemented` in `/r/index.json`
  records that for each. An error naming
  the namespace means `components.json` was not read.

  ### `add` behaves honestly [#add-behaves-honestly]

  Add any component and you get its source: the component file plus `lib/opsinjs.ts`
  and `lib/status.ts`, in the directories your `components.json` aliases name. Every
  id resolves to files today. Were a `planned` id added, the CLI should
  resolve the item and report that there is nothing to write, rather than creating
  an empty file.
</Steps>

## Troubleshooting [#troubleshooting]

**`add` wrote nothing.** Every id carries
files today, so an empty write points at your `components.json` instead. Confirm
its `aliases`, and check `implemented` for that name in `/r/index.json`.

**`@opsinjs` is not found.** Missing or malformed `registries` block. The `{name}`
placeholder is required; a URL without it cannot be substituted into. The same
error at the *second* step of an install has the same cause. At that step the
item resolved and one of its `registryDependencies` did not, so add the block
and retry.

**A file was written to the wrong directory.** The CLI uses the `aliases` in your
`components.json`. See
[components.json](../start/installation/components-json.mdx).

**I want a specific style.** Use the `/r/styles/<style>/<name>.json` path. The
docs URL for a component never carries a style segment. One canonical URL per
component was a deliberate decision, and the matrix lives on the machine
surfaces instead.

## Next [#next]

* [Namespaces](./namespaces.mdx) shows how to configure `@opsinjs` and compose
  registries.
* [registry.json](./registry-json.mdx) is the catalog, annotated field by field.
* [Upgrades and diffs](./upgrades-and-diffs.mdx) covers the fork problem, and
  the habit that makes it tractable.
