{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "shadcn-registry-item-code-block", "type": "registry:component", "registryDependencies": [ "button", "tree" ], "files": [ { "path": "components/domain-ui/shadcn-registry-item-code-block.tsx", "content": "\"use client\";\n\nimport { useEffect, useState, createContext, useContext } from \"react\";\nimport type { RegistryItem } from \"shadcn/registry\";\n// DynamicCodeBlock is not available, use CodeBlock instead\nimport { Button } from \"@/components/ui/button\";\nimport {\n TreeExpander,\n TreeIcon,\n TreeLabel,\n TreeNode,\n TreeNodeContent,\n TreeNodeTrigger,\n TreeProvider,\n TreeView,\n} from \"@/components/domain-ui/tree\";\nimport { PanelLeftClose, PanelLeft, FileText, File } from \"lucide-react\";\nimport { CodeBlock, Pre } from \"fumadocs-ui/components/codeblock\";\nimport { type ComponentProps, useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { DynamicCodeBlock } from \"fumadocs-ui/components/dynamic-codeblock\";\n\ninterface FileData {\n path: string;\n content?: string;\n}\n\ninterface FileTreeNode {\n path: string;\n type: \"file\" | \"directory\";\n children?: FileTreeNode[];\n content?: string;\n}\n\ninterface FileSelectionContextValue {\n selectedFilePath: string;\n setSelectedFilePath: (path: string) => void;\n}\n\nconst FileSelectionContext = createContext(\n null\n);\n\nfunction useFileSelection() {\n const context = useContext(FileSelectionContext);\n if (!context) {\n throw new Error(\n \"useFileSelection must be used within FileSelectionProvider\"\n );\n }\n return context;\n}\n\n// FilePath component - displays file path\ninterface FilePathProps {\n filePath?: string;\n}\n\nfunction FilePath({ filePath }: FilePathProps) {\n if (!filePath) {\n return null;\n }\n\n return (\n
\n
\n \n {filePath}\n
\n
\n );\n}\n\n// FileContent component - displays file content\ninterface FileContentProps {\n files: FileData[];\n}\n\nfunction pre(props: ComponentProps<\"pre\">) {\n return (\n \n
{props.children}
\n
\n );\n}\n\nfunction FileContent({ files }: FileContentProps) {\n const { selectedFilePath } = useFileSelection();\n\n const options = useMemo(\n () => ({\n components: {\n pre,\n },\n themes: {\n light: \"github-light\",\n dark: \"github-dark\",\n },\n }),\n []\n );\n\n const selectedFile = files.find((file) => file.path === selectedFilePath);\n\n if (!selectedFile) {\n return (\n
\n

Select a file to view its contents

\n
\n );\n }\n\n const language = selectedFile.path.split(\".\").pop()?.toLowerCase() || \"text\";\n\n return (\n
\n \n
\n );\n}\n\n// Create or get existing tree node\nfunction createOrGetTreeNode(\n currentPath: string,\n isLastPart: boolean,\n file: FileData,\n pathMap: Map,\n currentLevel: FileTreeNode[]\n): FileTreeNode {\n let existingNode = pathMap.get(currentPath);\n\n if (!existingNode) {\n const newNode: FileTreeNode = {\n path: currentPath,\n type: isLastPart ? \"file\" : \"directory\",\n children: isLastPart ? undefined : [],\n content: isLastPart ? (file.content ?? \"\") : undefined,\n };\n pathMap.set(currentPath, newNode);\n currentLevel.push(newNode);\n existingNode = newNode;\n }\n\n return existingNode;\n}\n\n// Process single file into tree\nfunction processFileIntoTree(\n file: FileData,\n tree: FileTreeNode[],\n pathMap: Map\n): void {\n const pathParts = file.path.split(\"/\");\n let currentPath = \"\";\n let currentLevel = tree;\n\n for (let i = 0; i < pathParts.length; i++) {\n const part = pathParts[i];\n if (!part) {\n continue;\n }\n\n currentPath = currentPath ? `${currentPath}/${part}` : part;\n const isLastPart = i === pathParts.length - 1;\n\n const existingNode = createOrGetTreeNode(\n currentPath,\n isLastPart,\n file,\n pathMap,\n currentLevel\n );\n\n if (!isLastPart && existingNode.children) {\n currentLevel = existingNode.children;\n }\n }\n}\n\n// Transform files into tree structure\nfunction transformFilesToTree(files: FileData[]): FileTreeNode[] {\n const tree: FileTreeNode[] = [];\n const pathMap = new Map();\n\n const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));\n\n for (const file of sortedFiles) {\n processFileIntoTree(file, tree, pathMap);\n }\n\n return tree;\n}\n\n// Get all directory paths for default expansion\nfunction getAllDirectoryIds(nodes: FileTreeNode[]): string[] {\n const ids: string[] = [];\n\n function traverse(nodeList: FileTreeNode[]) {\n for (const node of nodeList) {\n if (node.type === \"directory\") {\n ids.push(node.path);\n if (node.children) {\n traverse(node.children);\n }\n }\n }\n }\n\n traverse(nodes);\n return ids;\n}\n\n// Tree nodes component\nfunction TreeNodes({\n nodes,\n level = 1,\n}: {\n nodes: FileTreeNode[];\n level?: number;\n}) {\n const { setSelectedFilePath } = useFileSelection();\n\n return (\n <>\n {nodes.map((node, index) => {\n const nodeKey = node.path;\n const fileName = node.path.split(\"/\").pop() || \"\";\n const isLast = index === nodes.length - 1;\n\n if (node.type === \"file\") {\n return (\n \n setSelectedFilePath(node.path)}>\n \n \n }\n />\n {fileName}\n \n \n );\n }\n\n return (\n \n \n \n \n {fileName}\n \n \n {node.children && (\n \n )}\n \n \n );\n })}\n \n );\n}\n\n// FileTree component - displays file tree with toolbar\ninterface FileTreeProps {\n files: FileData[];\n collapsed: boolean;\n onToggleCollapse: () => void;\n selectedFilePath: string;\n}\n\nfunction FileTree({\n files,\n collapsed,\n onToggleCollapse,\n selectedFilePath,\n}: FileTreeProps) {\n if (collapsed) {\n return (\n
\n
\n \n \n \n
\n
\n );\n }\n\n const treeNodes = transformFilesToTree(files);\n const defaultExpandedIds = getAllDirectoryIds(treeNodes);\n\n return (\n
\n
\n \n Files ({files.length})\n \n \n \n \n
\n
\n \n \n \n \n \n
\n
\n );\n}\n\nasync function fetchRegistryItem(url: string): Promise {\n const response = await fetch(url, { cache: \"force-cache\" });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch registry item: ${response.status}`);\n }\n\n const registryItem: RegistryItem = await response.json();\n\n if (!registryItem) {\n throw new Error(`Registry item not found at \"${url}\"`);\n }\n\n return registryItem;\n}\n\n// Main component\ninterface ShadcnRegistryItemCodeBlockProps {\n url: string;\n}\n\nexport function ShadcnRegistryItemCodeBlock({\n url,\n}: ShadcnRegistryItemCodeBlockProps) {\n const [files, setFiles] = useState([]);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState(null);\n const [selectedFilePath, setSelectedFilePath] = useState(\"\");\n const [isTreeCollapsed, setIsTreeCollapsed] = useState(false);\n\n useEffect(() => {\n let isMounted = true;\n\n fetchRegistryItem(url)\n .then((registryItem) => {\n if (!registryItem.files || registryItem.files.length === 0) {\n throw new Error(\"No files found in registry item\");\n }\n\n const fileData: FileData[] = registryItem.files.map((file) => ({\n path: file.path,\n content: file.content,\n }));\n\n if (isMounted) {\n setFiles(fileData);\n setSelectedFilePath(fileData[0]?.path || \"\");\n }\n })\n .catch((err) => {\n if (isMounted) {\n const errorMessage = err instanceof Error ? err.message : String(err);\n setError(errorMessage);\n }\n })\n .finally(() => {\n if (isMounted) {\n setIsLoading(false);\n }\n });\n\n return () => {\n isMounted = false;\n };\n }, [url]);\n\n if (isLoading) {\n return (\n
\n

\n Loading registry files...\n

\n
\n );\n }\n\n if (error) {\n return (\n
\n

\n Error loading registry: {error}\n

\n
\n );\n }\n\n const fileSelectionValue = {\n selectedFilePath,\n setSelectedFilePath,\n };\n\n // Single file layout\n if (files.length === 1) {\n return (\n \n
\n \n \n
\n
\n );\n }\n\n // Multiple files layout\n return (\n \n
\n {/* */}\n setIsTreeCollapsed(!isTreeCollapsed)}\n selectedFilePath={selectedFilePath}\n />\n
\n \n \n
\n
\n
\n );\n}\n", "type": "registry:component" } ], "meta": { "tags": [ "shadcn", "registry", "code-block", "file-tree", "code-viewer", "syntax-highlighting", "tree-view" ] } }