Contribution guidelines for the project.
This project uses pnpm as the package manager; you must have pnpm
installed globally to run commands and tests locally.
Here is where each type of file lives:
src/registry/base holds the components that ship to users.src/registry/hooks and src/registry/lib hold code shared between components.src/registry/demos holds demo components and their controls.src/registry/index.ts declares every component and its dependencies.src/content holds documentation.(src/registry)
|-- base
| |-+ your-component
| |-+ your-component.tsx # Your new component
|-- hooks
| |-+ use-frame-loop.ts # Shared hooks
|-- demos
| |-+ your-component
| | |-+ your-component.tsx # Your new demo component
| | |-+ controls.ts # Your demo controls
| |
| |-- index.ts # Exports the demo components
(src/content)
|-- en
|-- components
|-- _dir.yml
|-+ your-component
|-+ your-component.mdx # Your new component documentationEach component has a single responsibility and lives in a single file, so it stays self-contained and easy to reuse.
Every base component starts with "use client".
Components run hooks and browser APIs, and users drop them into server components, which is the default in the Next.js app router.
"use client"
const NAMED_X_CONSTANT = {...} as const
const NAMED_Y_CONSTANT = "value" as const
function helperFunction() {...}
type ComponentExampleProps = {...} & ComponentProps<'div'>
export function ComponentExample({}: ComponentExampleProps) {...}When logic is shared across components, put it in src/registry/hooks or src/registry/lib and list the file in the component's shared field.
The use-frame-loop hook is one example.
This project uses motion.dev for DOM-based animations, React Three Fiber for WebGL effects, and Tailwind CSS for styling.
Vanilla JS animations such as requestAnimationFrame and the Web Animations API can also be used where a library isn't needed.
Anything listed in a component's dependencies is installed on the user's machine, so the list has to be complete.
A component that imports Three.js types declares @types/three next to three.
Tests run automatically on every pull request. They verify that every component has its files, demo, export, and documentation in sync. Unit tests per component are not required by default.
Use the pnpm scafold-new command.
It creates the base component, the demo, a templated MDX doc, and patches the registry and demo index.
pnpm scafold-new animated-text --category scroll
pnpm scafold-new sparkle --category backgroundIf you prefer to do it manually, follow the steps below.
Create a folder in src/registry/base named after the component:
"use client"
export function ComponentName() {
return <></>;
}Create a demo in src/registry/demos and structure it how you want:
import { ComponentName } from "@/registry/base/component-name/component-name";
export default function ComponentNameDemo() {
return <ComponentName />;
}Export the demo in src/registry/demos/index.ts:
export const demos: Record<
string,
React.LazyExoticComponent<React.ComponentType>
> = {
"component-name": lazy(() => import("./component-name/component-name")),
};Declare the component in src/registry/index.ts:
export const components: TRegistryComponent[] = [
{
name: "new-component",
files: ["new-component.tsx"],
description: "My new component",
shared: ["hooks/use-my-hook.ts"],
dependencies: ["motion"],
registryDependencies: ["text-split"],
},
];shared takes full paths with their extension, relative to src/registry.
registryDependencies names other registry components, which the CLI installs alongside this one.
A component page opens on its preview, then folds the prose into collapsible sections. The order is frontmatter, install guide, Usage, API, and the components it builds on.
Add the metadata for the component:
---
title: "New Component"
description: "My new component"
icon: "Component"
---The preview is built by the doc page from the registry, so the document does not declare it.
Add the installation guide with the <InstallGuide /> component, using the exact name of the base component.
It renders the agent prompt, with the CLI command and the file-by-file copy collapsed underneath it.
<InstallGuide name="component-name" />Wrap the prose in <Collapsible /> so the page opens on the preview:
<Collapsible title="Usage">
Import paths repeat the component name, because each file installs to
`components/<name>/<name>.tsx`:
```tsx
import { ComponentName } from "@/components/component-name/component-name";
```
</Collapsible>
---
<Collapsible title="API" defaultOpen>
| Name | Type | Default | Description |
| ------- | -------- | --------- | ---------------------------------------- |
| `speed` | `number` | `1` | Controls the animation speed multiplier. |
| `color` | `string` | `#ff5733` | Sets the primary color of the component. |
</Collapsible>Close the page with <BuiltOn />, which links the component to the primitives it is built from:
<BuiltOn name="component-name" />Controls let users change props on the live preview, and the values travel into the copyable agent prompt.
Declare them in src/registry/demos/<component-name>/controls.ts and register the file in src/registry/demos/controls.ts.
The values have to match the default props of the base component:
import type { ControlDef } from "@/types/controls"
export const controls: Record<string, ControlDef> = {
speed: { type: "slider", value: 1, min: 0, max: 10, step: 0.1 },
color: { type: "color", value: "#ff5733" },
palette: { type: "palette", value: ["#ff5733", "#33c1ff"], min: 2, max: 5 },
variant: { type: "select", value: "solid", options: ["solid", "outline"] },
enabled: { type: "boolean", value: true },
glow: { type: "slider", value: 0.5, min: 0, max: 1, step: 0.01, showIf: "enabled" },
}| Type | Fields | Renders |
|---|---|---|
slider | value min max step | Slider |
color | value | Color picker |
palette | value min max | Color list |
select | value options | Select |
boolean | value | Toggle switch |
Any control takes an optional showIf, naming a boolean control that has to be on for it to render.
Export the base component's props type, then spread controls onto it:
export default function ComponentNameDemo(
controls: Partial<ComponentNameProps>,
) {
return <ComponentName {...controls} />;
}