# Registry & distribution

> Lopifi distributes through the shadcn registry protocol. Components are copied into the consumer's repo; they own the code from that moment.

Source: https://lopifi.com/docs/architecture/01-registry-and-distribution
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

# Registry & distribution

Lopifi distributes through the **shadcn registry protocol**. Components are copied into the consumer's repo; they own the code from that moment.

Don't reinvent this. shadcn's model is correct for a domain library specifically because finance teams *must* modify — rounding policy, fiscal calendar, categorization rules, compliance copy. A black-box npm package is the wrong shape for something that has to be edited.

---

## Install shape

```bash
npx shadcn@latest add @lopifi/balance-card
npx shadcn@latest add @lopifi/profit-and-loss
npx shadcn@latest add @lopifi/swap-widget
```

Consumer config:
```json
// components.json
{
  "registries": {
    "@lopifi": "https://lopifi.com/r/{name}.json"
  }
}
```

`registryDependencies` resolve automatically, including shadcn's own primitives — installing `@lopifi/transaction-table` pulls `@lopifi/money`, `@lopifi/status-pill`, and shadcn's `table`, `checkbox`, `dropdown-menu`.

---

## registry.json

```json
{
  "$schema": "https://ui.shadcn.com/schema/registry.json",
  "name": "ledger",
  "homepage": "https://lopifi.com",
  "items": [
    {
      "name": "theme",
      "type": "registry:theme",
      "title": "Lopifi theme",
      "description": "Design tokens: color, spacing, radius, motion, typography.",
      "cssVars": { "light": {}, "dark": {} },
      "css": {}
    },
    {
      "name": "money",
      "type": "registry:component",
      "title": "Money",
      "description": "Renders a monetary value with correct formatting, sign, and alignment.",
      "dependencies": [],
      "registryDependencies": ["@lopifi/theme"],
      "files": [
        { "path": "components/ledger/money.tsx",  "type": "registry:component" },
        { "path": "lib/ledger/format-money.ts",   "type": "registry:lib" },
        { "path": "lib/ledger/money.types.ts",    "type": "registry:lib" }
      ]
    },
    {
      "name": "balance-card",
      "type": "registry:block",
      "title": "Balance card",
      "description": "Account balance with delta and optional sparkline.",
      "registryDependencies": ["@lopifi/money", "@lopifi/delta", "@lopifi/sparkline", "card"],
      "files": [{ "path": "components/ledger/balance-card.tsx", "type": "registry:component" }]
    }
  ]
}
```

### Item types

| Type | Use |
|---|---|
| `registry:theme` | Token layer. Installed first, once. |
| `registry:lib` | Pure utilities — formatters, allocation, date helpers |
| `registry:hook` | `use-density`, `use-live-value`, `use-column-priority` |
| `registry:ui` | Primitives — `Money`, `Delta`, `Figure`, `StatusPill` |
| `registry:component` | Composed but single-purpose — `TransactionRow`, `AmountInput` |
| `registry:block` | Full surfaces — `ProfitAndLoss`, `SwapWidget`, `DashboardShell` |
| `registry:page` | Complete routes — `payments-page`, `close-dashboard` |

---

## Dependency graph

Strictly layered. **Dependencies point downward only** — a cycle here means an item can't be installed independently.

```
Layer 0   theme                  tokens, CSS vars, Tailwind preset
             ↑
Layer 1   lib                    format-money · allocate · fiscal-period · money.types
             ↑
Layer 2   hooks                  use-density · use-live-value · use-reduced-motion
             ↑
Layer 3   primitives             Money · Delta · Figure · StatusPill · Sparkline · Rule
             ↑
Layer 4   components             TransactionRow · AmountInput · StatementRow · RateLine
             ↑
Layer 5   blocks                 TransactionTable · ProfitAndLoss · SwapWidget · BalanceCard
             ↑
Layer 6   pages                  Dashboard · Payments · Close
```

**Every item is installable on its own.** `@lopifi/money` pulls only `theme`. This is what makes adoption incremental — a team can take just the money primitive and nothing else.

---

## The primitive dependency

Layers 3–6 sit on accessible primitives we don't own, and that dependency currently has a migration running through it. A copy-paste registry needs an answer before a consumer hits it.

**Base UI is the destination; Radix is what most consumers have installed today.** shadcn's own primitives are mid-migration, so `@lopifi/transaction-table` may land on either, depending on when the consumer ran `shadcn add dropdown-menu`. Our position:

- **Lopifi items never import a primitive directly.** They import the consumer's shadcn wrapper (`@/components/ui/dialog`), so whichever primitive that wrapper resolves to is the one that gets used — and we never pin a version of somebody else's dependency inside a file the consumer owns.
- **Nothing in our code touches primitive-specific internals**, with one exception: the transform-origin variable, which is [tokenized for both](../motion/02-tokens.md).
- **Never two primitive systems in one interaction surface.** A Radix dialog containing a Base UI select fights over focus. Stated in [accessibility](../craft/02-accessibility.md); enforced here by the import rule above.
- **When a consumer migrates**, `shadcn-ui/ui`'s `migrate-radix-to-base` skill rewrites the wrappers and our items need no change at all. **That is the test of whether the rule is holding: if a Base UI migration requires editing a `@lopifi/*` file, we imported something we shouldn't have.**

The scratch-app step in [repo hosting](#repo-hosting) runs on both primitive sets. If it only ever runs on one, this rule is documentation rather than a constraint.

---

## The theme item

Installed first, and it's the one thing that must not be forked casually.

```css
/* app/globals.css — installed by @lopifi/theme */
@layer base {
  :root {
    /* color */
    --canvas: oklch(0.985 0.002 265);
    --surface: oklch(1 0 0);
    /* … full set from foundations/03-color.md … */

    /* space, radius, type, motion, z — all from foundations/ */
  }
  [data-theme="dark"] { /* … */ }
  [data-value-palette="signal"] { /* … */ }
}
```

Plus a Tailwind preset so the tokens are reachable as utilities:

```ts
// tailwind-ledger-preset.ts
export default {
  theme: {
    extend: {
      colors: { canvas: "var(--canvas)", surface: "var(--surface)", /* … */ },
      borderRadius: { xl: "24px", /* … */ },
      transitionTimingFunction: { out: "cubic-bezier(0.23,1,0.32,1)", /* … */ },
    },
  },
};
```

**Tailwind v4** consumers get `@theme` in CSS with no JS config at all — preferred, and the registry ships both.

---

## Versioning

Copy-paste distribution means there's no upgrade path — which is the trade for ownership. Mitigations:

1. **Semantic versioning on the registry**, surfaced per item, with the version stamped in a header comment in every generated file.
2. **A changelog per item**, not just per release, so `npx @lopifi/diff balance-card` can show what changed since the version in your repo.
3. **A codemod for breaking token renames.** Token changes are the only breaking change that can be mechanized, and they're the most common.
4. **Additive-only within a major.** New props, new variants, new items. Never a changed default, never a removed prop.
5. **The `theme` item is the stable contract.** Components change; token *names* don't, within a major.

---

## Docs site

Per the Sonner principle: **build a great documentation site** — let people touch the thing before they adopt it. This is a milestone, not an afterthought, and it's how a component library gets used.

- **Every component is live and interactive**, with the props panel wired to real controls.
- **Every example is copy-paste-ready**, with the install command next to it.
- **Every example includes the edge cases** — the negative, the null, the JPY, the very long label. That's the differentiation, so show it rather than describing it.
- **A theme toggle, a density toggle, a signal-palette toggle, and a reduced-motion toggle** on every example. Users should be able to see the system's claims hold.
- **Motion specs are demonstrable** — a slow-motion toggle on every animated example, so the craft is inspectable.
- **The design docs ship with the site**, not just in the repo. The guidelines are part of the product.
- Built with the Next.js App Router, rendering the Markdown specs with `marked`, deployed on Vercel. The site is also the visual regression target.

### Metadata
Every component page is a shareable link, and the site is the product's entire discovery surface — so metadata ships with the page rather than being retrofitted onto it. Audited against `ibelick/ui-skills` (`fixing-metadata`).

- **One source per page.** Next.js `generateMetadata`, never a competing `<head>`. Duplicate `title` or `canonical` tags are the failure mode here and they fail silently.
- **`title`, `description`, `canonical` and `og:url` must agree.** A page that canonicalizes to `/docs/money` while its `og:url` says `/components/money` has told the index and the share preview two different things about itself.
- **Open Graph images are absolute, `summary_large_image`, and generated per component** — the component rendered on its own surface, with a negative in it. This is a component library; the preview should show a component, not a logo.
- **Preview deployments are `noindex`.** Every Vercel preview is a complete duplicate of the docs; left indexable, they compete with the real site for its own terms.
- **No JSON-LD we can't back.** `SoftwareSourceCode` on a component page is honest. Invented ratings are not.

Scoped to the site: nothing in the registry emits metadata, and no `@lopifi/*` component ever sets a page title.

---

## Repo hosting

Private GitHub repo; the project is not open source. Components reach consumers only through the registry, under the site's terms of use. `registry.json` at the root, built artifacts served from `/r/{name}.json` on the docs domain, generated by `bun run gen` (`scripts/build-registry.mjs`).

Today `.github/workflows/ci.yml` runs generate → lint → typecheck → test → build. The target registry pipeline adds the scratch-app install:

```
.github/workflows/registry.yml
  → shadcn build
  → validate every item resolves
  → install every item into a scratch Next.js app — once on Radix, once on Base UI
  → typecheck both scratch apps
  → deploy
```

That third step is the one that matters: **CI installs every registry item into a clean app and typechecks it.** A registry that references a file that doesn't exist is the most common failure mode, and it's silent until a user hits it.
