I keep rebuilding the same buttons. Same warm palette, same type pairing, same small decisions about borders and focus rings, copied from one project to the next and drifting a little each time. So I did the boring, useful thing: I put those components in one place and made them installable with the shadcn CLI. That place is `ravenwood-ui`, a personal shadcn registry. It isn't a product, and it isn't a launch. It's how I stop copy-pasting my own work.
What this is (and isn't)
`ravenwood-ui` lives on GitHub at `raythurman2386/ravenwood-ui`. The repo is public, so anyone can read it or pull from it. The `package.json` is set to `"private": true`, though, and that's on purpose. I don't publish it to npm, there's no versioned design-system package, and there's no support promise attached to it.
It's a shadcn registry: a set of JSON files the shadcn CLI knows how to read. When a project needs a component, the CLI copies the source into that project, and from then on the project owns the code. That fits how I already work. I want the code to sit in the app, where I can read it and change it, not behind a dependency I have to work around.
If you want to see it before reading the rest, there's a live preview at ui.ravenwoodsoftware.dev.
Naming: cabin vs radix-nova vs @ravenwood
The project has three names in it, and each one means something different:
- radix-nova is the shadcn CLI style. I kept Radix primitives and the `radix-nova` style rather than inventing a style of my own. Consuming projects set `"style": "radix-nova"` in `components.json`, so whatever the CLI generates matches what the registry expects.
- Cabin is the look: the tokens on top of that style. Fraunces handles display type and Figtree handles body text. The colors are named for what they look like: bark, canopy, lantern, moss, and rust.
- `@ravenwood` is the registry namespace, the prefix you type when you install something, as in `@ravenwood/<name>`.
Splitting them up this way keeps each layer honest. Radix and radix-nova cover structure and behavior. Cabin covers how things look. `@ravenwood` is where they come from.
The cabin colors weren't invented for this registry. Several of my internal apps already use the Ravenwood colors, and so do my `ravenwood-vscode` theme and my omarchy themes. The registry is built on that existing palette. As I take on more client work, I'm gradually bringing the registry into those projects too.
What's in the catalog
The registry has 64 catalog items under `@ravenwood` right now. Each one is a JSON file at `public/r/{name}.json`, served raw from the repo. Nothing more is needed. There's no registry server and no API, just static JSON the CLI fetches.
Two things were still in progress when I first wrote this, and both have since shipped (October 8, 2026):
- Carousel: an Embla-based carousel (slide or fade, items per view by breakpoint, opt-in autoplay with a pause control) is now in the catalog as `@ravenwood/carousel`.
- Maps: a MapLibre map vendored from mapcn (markers, popups, routes, GeoJSON, clusters, and controls) is now in the catalog as `@ravenwood/map`.
Both went through the same publish → consume loop described below before they landed.
How publish and consume actually work
Both sides are short.
Publishing. In the registry repo:
pnpm registry:buildThat script runs `shadcn build`, which reads `registry.json` and the component sources and writes the built item files to `public/r/`. Then I commit and push three things together:
- `registry.json`
- the component sources
- `public/r/`
Pushing is the release. Once the JSON is on GitHub, it's live.
Consuming. A project can be set up from the registry's index, or point at the registry and add items one at a time. In either case, `components.json` gets a `@ravenwood` registry entry that points at the raw `public/r/{name}.json` URLs:
{
"style": "radix-nova",
"registries": {
"@ravenwood": "https://raw.githubusercontent.com/raythurman2386/ravenwood-ui/main/public/r/{name}.json"
}
}Then adding a component looks like this:
npx shadcn@latest add @ravenwood/<name>When I change something in the registry and want the update in an app, I pull it again:
npx shadcn@latest add @ravenwood/<name> --overwriteThat covers the whole workflow.
Proof: two consumers, not a launch
I didn't want this to stay a nice idea with nothing using it, so two of my own projects use it now:
- `ravenwood-software`, the agency site, adopted it in PR #13 (around September 30, 2026).
- `ravenwood`, my portfolio, adopted it in PR #207 (around October 2, 2026). That PR also moved the portfolio off Bootstrap.
Both projects have `components.json` set to `radix-nova` with the `@ravenwood` registry URL. Each one has a small set of UI components installed, only what it needs right now. Neither project has signature `components/ravenwood` pieces checked in yet.
That's all the proof there is: two real projects, both mine, using the registry the way it was built to be used. It's enough to show the publish → consume → update loop works outside the repo it lives in.
Where apps intentionally diverge
The main design decision across these projects is about light and dark mode. In the registry itself, dark is the default.
The registry's preview site uses next-themes with `defaultTheme` set to `dark` and `enableSystem` turned off, so it opens in dark mode regardless of your OS setting. There's a theme toggle in the site header if you'd rather switch to light. Both modes are built deliberately. Neither one is a fallback.
That's the line between what's shared and what's local. Tokens and components are shared, and the `theme` and `index` registry items carry both the dark and light token sets. Layout, content, how an app handles light and dark mode, and how much of the catalog an app pulls in are up to each app.
The update loop (why this stays small)
The registry stays manageable because of how updates work:
- Change a component in `ravenwood-ui`.
- Run `pnpm registry:build`.
- Push.
- In any app that wants the change, run `shadcn add @ravenwood/<name> --overwrite` and review the diff.
There's no version bump, changelog ritual, or release pipeline. Each app gets the change when I decide to pull it, and I see exactly what changed in a normal code review.
That's also why this isn't turning into a design-system company. A packaged design system needs versioning, deprecation policies, and a migration story, because the people consuming it aren't the people maintaining it. Here, those are mostly the same person: me.
What we deliberately skip
Some things I'm choosing not to do, at least for now:
- No npm package. The repo is `private: true` and stays that way. Distribution happens through the shadcn CLI and raw JSON.
- No big-bang install. Signature components and the fuller catalog arrive in each project gradually, as the work calls for them. That's why neither consumer has `components/ravenwood` signatures checked in yet. It's the same pace at which I'm bringing the registry into client work.
- No early shipping. The Embla carousel and the map components stayed out of the catalog until they were ready, and only landed once they were.
- No version pinning (for now). I could pin consumers to a tag or commit, and nothing stops me. But I expect to be the main consumer, and with the shadcn CLI, `add --overwrite` plus a diff review is easy enough that tracking `main` is the honest setup. If someone other than me starts depending on this, I'll revisit that.
Closing
It's a small project. It's a folder of JSON files, a build script, and a few CLI commands that let me reuse my own work without pretending it's a product. If it ever needs to grow into something more formal, the code is all there and easy to read. Until then, it does what I need: my projects look like they belong together, and I'm not rebuilding the same button anymore.