āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā š shadcn/directory/crafter-station/elements/supabase_registry_analysis ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
After deep analysis of /supabase/apps/ui-library, here are the critical patterns we need to adopt:
registry/
āāā default/
āāā blocks/
āāā clerk/ # ā Provider subdirectories
ā āāā clerk-sign-in-shadcn/
ā āāā clerk-sign-up-shadcn/
āāā tinte/
āāā polar/
registry/
āāā index.ts # Main export, imports from organizer files
āāā blocks.ts # Organizes all blocks
āāā clients.ts # Framework-specific clients
āāā examples.ts # Demo components for documentation
āāā platform.ts # Platform-specific components
āāā utils.ts # Registry utilities (registryItemAppend)
āāā default/
āāā blocks/ # ā
FLAT structure, no provider subdirs
ā āāā dropzone/
ā āāā realtime-cursor/
ā āāā current-user-avatar/
ā āāā password-based-auth-nextjs/
āāā clients/
ā āāā nextjs/
ā āāā react/
ā āāā react-router/
ā āāā tanstack/
āāā examples/ # Demo/preview components
ā āāā dropzone-demo.tsx
ā āāā realtime-cursor-demo.tsx
āāā fixtures/ # Shared types, database schemas
Key Insight: Components are FLAT, not nested by provider. Provider info is in the name.
import { type RegistryItem } from 'shadcn/schema'
import { clients } from './clients'
import { registryItemAppend } from './utils'
// Import registry-item.json files
import dropzone from './default/blocks/dropzone/registry-item.json' with { type: 'json' }
import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' with { type: 'json' }
// Helper to combine component with all clients
const combine = (component: RegistryItem) => {
return clients.flatMap((client) => {
return registryItemAppend(
{
...component,
name: `${component.name}-${client.name.replace('supabase-client-', '')}`,
},
[client]
)
})
}
// Specific client selection
const nextjsClient = clients.find((client) => client.name === 'supabase-client-nextjs')
export const blocks = [
// Auto-combine with all clients
...combine(dropzone as RegistryItem),
...combine(realtimeCursor as RegistryItem),
// Manual framework-specific combination
registryItemAppend(passwordBasedAuthNextjs as RegistryItem, [nextjsClient!]),
] as RegistryItem[]
import type { RegistryItem } from 'shadcn/schema'
import nextjs from './default/clients/nextjs/registry-item.json' with { type: 'json' }
import react from './default/clients/react/registry-item.json' with { type: 'json' }
export const clients = [nextjs, react] as RegistryItem[]
import type { RegistryItem } from 'shadcn/schema'
export const examples: RegistryItem[] = [
{
name: 'dropzone-demo',
type: 'registry:example',
registryDependencies: [],
files: [
{
path: 'registry/default/examples/dropzone-demo.tsx',
type: 'registry:example',
},
],
},
]
import { type Registry, type RegistryItem } from 'shadcn/schema'
import { examples } from '@/registry/examples'
import { blocks } from './blocks'
import { clients } from './clients'
export const registry = {
name: 'Elements',
homepage: 'https://tryelements.dev',
items: [
...blocks,
...clients,
...examples, // Internal use only
],
} satisfies Registry
__registry__ PatternThe __registry__/index.tsx file is AUTO-GENERATED and used for preview components in documentation.
import { registry } from '../registry/index'
// 1. Write public/r/registry.json (without examples)
const cleanedRegistry = {
$schema: 'https://ui.shadcn.com/schema/registry.json',
...registry,
items: registry.items.filter((item) => item.type !== 'registry:example'),
}
fs.writeFileSync('public/r/registry.json', JSON.stringify(cleanedRegistry, null, 2))
// 2. Generate __registry__/index.tsx for ComponentPreview
const registryIndex = `
// @ts-nocheck
// This file is autogenerated by scripts/build-registry.mts
import * as React from "react"
export const Index = {
"default": {
${registry.items
.filter((item) => item.type === 'registry:example')
.map((item) => {
const componentFile = item.files.find((file) => file.path.endsWith('.tsx'))
return `
"${item.name}": {
component: React.lazy(() => import("@/${componentFile.path}")),
}`
})}
},
} as const
`
fs.writeFileSync('__registry__/index.tsx', registryIndex)
// components/component-preview.tsx
import { Index } from '@/__registry__'
export function ComponentPreview({ name }) {
const Component = Index.default[name]?.component
return (
<Suspense fallback={<div>Loading...</div>}>
<Component />
</Suspense>
)
}
// registry/default/blocks/dropzone/components/dropzone.tsx
// ā
Import from registry using @/registry
import { useSupabaseUpload } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'
import { Button } from '@/registry/default/components/ui/button'
// ā
Import utilities from lib
import { cn } from '@/lib/utils'
// registry/default/examples/dropzone-demo.tsx
// ā
Import the actual registry component
import { Dropzone } from '@/registry/default/blocks/dropzone/components/dropzone'
import { useSupabaseUpload } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'
export default function DropzoneDemo() {
const props = useSupabaseUpload({ bucketName: 'test' })
return <Dropzone {...props} />
}
You want dropzone to work with Next.js, React, TanStack, etc.
registry/default/blocks/dropzone/registry/default/clients/[framework]/registryItemAppend() to combine themdropzone-nextjs = dropzone + supabase-client-nextjsdropzone-react = dropzone + supabase-client-reactdropzone-tanstack = dropzone + supabase-client-tanstackAll created automatically by the combine() function!
registry/default/blocks/dropzone/
āāā registry-item.json # Manifest
āāā components/ # React components
ā āāā dropzone.tsx
āāā hooks/ # Custom hooks
ā āāā use-supabase-upload.ts
āāā lib/ # Utilities (optional)
āāā utils.ts
registry/default/clients/nextjs/
āāā registry-item.json
āāā lib/
āāā supabase/
āāā client.ts # Browser client
āāā server.ts # Server client
āāā middleware.ts # Middleware
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "dropzone",
"type": "registry:component",
"title": "Dropzone (File Upload)",
"description": "Displays a control for easier uploading of files",
"registryDependencies": ["button"],
"dependencies": ["react-dropzone", "lucide-react"],
"files": [
{
"path": "registry/default/blocks/dropzone/components/dropzone.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/dropzone/hooks/use-supabase-upload.ts",
"type": "registry:hook"
}
]
}
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "supabase-client-nextjs",
"type": "registry:lib",
"title": "Supabase Client for Next.js",
"registryDependencies": [],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
"envVars": {
"NEXT_PUBLIC_SUPABASE_URL": "",
"NEXT_PUBLIC_SUPABASE_ANON_KEY": ""
},
"files": [
{
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
"type": "registry:lib"
}
]
}
registry/default/blocks/clerk/clerk-sign-in/ to registry/default/blocks/clerk-sign-in/blocks.ts, examples.tsregistry/default/examples/__registry__/index.tsx@/registry/default/blocks/[component]/@/components/ui/ or @/lib/registry/default/blocks/my-component/bun run build:registrypublic/r/registry.json - Main index (without examples)public/r/[name].json - Individual component files__registry__/index.tsx - Preview component loaderā
FLAT structure - No provider subdirectories in blocks
ā
Organizer files - blocks.ts, examples.ts separate concerns
ā
Auto-generation - __registry__/index.tsx generated for previews
ā
Examples separate - Demo components in their own directory
ā
JSON imports - Direct import with with { type: 'json' }
ā
Type safety - Use satisfies Registry for type checking
This structure is production-proven by Supabase and follows shadcn best practices exactly.
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā