{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-system-light",
  "title": "File System Light",
  "description": "A minimal file tree in a ViewerSidebar with FileViewer in the ViewerSurface.",
  "dependencies": [
    "@pierre/trees",
    "lucide-react@^0.460.0"
  ],
  "registryDependencies": [
    "@retab/utils",
    "breadcrumb",
    "@retab/file-viewer"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/file-system-light.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { ViewerSource } from \"@/lib/viewer-source\";\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { FileViewerPreview } from \"@/components/ui/file-viewer\";\nimport {\n  ViewerBody,\n  ViewerHeader,\n  ViewerRoot,\n  ViewerSidebar,\n  ViewerSurface,\n} from \"@/components/ui/viewer\";\n\nimport { FileSystemLightTree } from \"./file-system-light-tree\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport type FileSystemLightFile = {\n  path: string;\n  source: ViewerSource;\n};\n\nexport type FileSystemLightProps = {\n  files: FileSystemLightFile[];\n  className?: string;\n  defaultSelectedPath?: string | null;\n  onSelectedPathChange?: (path: string | null) => void;\n  selectedPath?: string | null;\n  title?: string;\n};\n\nexport function FileSystemLight({\n  files,\n  className,\n  defaultSelectedPath = files[0]?.path ?? null,\n  onSelectedPathChange,\n  selectedPath: selectedPathProp,\n  title = \"Files\",\n}: FileSystemLightProps) {\n  const normalizedFiles = React.useMemo(\n    () =>\n      files.map((file) => ({\n        ...file,\n        path: normalizeFileSystemLightPath(file.path),\n      })),\n    [files],\n  );\n  const paths = React.useMemo(\n    () => normalizedFiles.map((file) => file.path),\n    [normalizedFiles],\n  );\n  const filesByPath = React.useMemo(\n    () => new Map(normalizedFiles.map((file) => [file.path, file])),\n    [normalizedFiles],\n  );\n  const onSelectedPathChangeRef = React.useRef(onSelectedPathChange);\n  const isSelectionControlledRef = React.useRef(selectedPathProp !== undefined);\n  const initialSelectedPath =\n    normalizeOptionalFileSystemLightPath(defaultSelectedPath);\n  const [internalSelectedPath, setInternalSelectedPath] = React.useState<\n    string | null\n  >(() =>\n    initialSelectedPath && filesByPath.has(initialSelectedPath)\n      ? initialSelectedPath\n      : (paths[0] ?? null),\n  );\n  const isSelectionControlled = selectedPathProp !== undefined;\n  const selectedPath = normalizeOptionalFileSystemLightPath(\n    selectedPathProp ?? internalSelectedPath,\n  );\n  const selectedFile = selectedPath ? filesByPath.get(selectedPath) : undefined;\n  const selectedTreePaths = React.useMemo(\n    () => (selectedPath && filesByPath.has(selectedPath) ? [selectedPath] : []),\n    [filesByPath, selectedPath],\n  );\n  const selectedPathSegments = React.useMemo(\n    () => getFileSystemLightPathSegments(selectedPath),\n    [selectedPath],\n  );\n\n  useKeyedMountEffect(joinEffectKey([onSelectedPathChange]), () => {\n    onSelectedPathChangeRef.current = onSelectedPathChange;\n  });\n\n  useKeyedMountEffect(joinEffectKey([isSelectionControlled]), () => {\n    isSelectionControlledRef.current = isSelectionControlled;\n  });\n\n  return (\n    <ViewerRoot\n      data-viewer=\"file-system-light\"\n      className={cn(\n        \"bg-background text-foreground h-[640px] overflow-hidden rounded-md border\",\n        className,\n      )}\n    >\n      <ViewerHeader>\n        <div className=\"flex h-12 min-w-0 items-center justify-between gap-3 px-3\">\n          <Breadcrumb className=\"min-w-0\">\n            <BreadcrumbList className=\"min-w-0 flex-nowrap gap-1 overflow-hidden text-sm sm:gap-1.5\">\n              {selectedPathSegments.map((segment, index) => {\n                const isLast = index === selectedPathSegments.length - 1;\n\n                return (\n                  <React.Fragment key={`${segment}-${index}`}>\n                    <BreadcrumbItem\n                      className={cn(\"min-w-0\", !isLast && \"shrink-0\")}\n                    >\n                      {isLast ? (\n                        <BreadcrumbPage className=\"truncate\">\n                          {segment}\n                        </BreadcrumbPage>\n                      ) : (\n                        <span className=\"text-muted-foreground\">{segment}</span>\n                      )}\n                    </BreadcrumbItem>\n                    {!isLast ? <BreadcrumbSeparator /> : null}\n                  </React.Fragment>\n                );\n              })}\n            </BreadcrumbList>\n          </Breadcrumb>\n          <div className=\"text-muted-foreground shrink-0 text-xs\">\n            {paths.length} files\n          </div>\n        </div>\n      </ViewerHeader>\n      <ViewerBody>\n        <ViewerSidebar\n          aria-label={title}\n          collapsible=\"none\"\n          width=\"20rem\"\n          className=\"flex min-h-0 flex-col border-r\"\n        >\n          <FileSystemLightTree\n            aria-label={title}\n            className=\"block min-h-0 flex-1\"\n            onSelectedPathsChange={(selectedPaths) => {\n              const nextPath = selectedPaths.at(-1) ?? null;\n\n              if (!isSelectionControlledRef.current) {\n                setInternalSelectedPath(nextPath);\n              }\n              onSelectedPathChangeRef.current?.(nextPath);\n            }}\n            paths={paths}\n            selectedPaths={selectedTreePaths}\n            style={\n              {\n                \"--trees-bg-override\": \"transparent\",\n                \"--trees-border-color-override\": \"transparent\",\n                \"--trees-fg-override\": \"var(--foreground)\",\n                \"--trees-fg-muted-override\": \"var(--muted-foreground)\",\n                \"--trees-focus-ring-color-override\": \"var(--ring)\",\n                \"--trees-focus-ring-width-override\": \"2px\",\n                \"--trees-item-padding-x-override\": \"0.75rem\",\n                \"--trees-border-radius-override\": \"0.375rem\",\n                \"--trees-file-icon-color\": \"var(--muted-foreground)\",\n                \"--trees-selected-bg-override\": \"var(--primary)\",\n                \"--trees-selected-fg-override\": \"var(--primary-foreground)\",\n              } as React.CSSProperties\n            }\n          />\n        </ViewerSidebar>\n        <ViewerSurface>\n          {selectedFile ? (\n            <FileViewerPreview\n              source={selectedFile.source}\n              className=\"size-full min-h-0\"\n            />\n          ) : (\n            <div className=\"text-muted-foreground flex size-full items-center justify-center text-sm\">\n              Select a file\n            </div>\n          )}\n        </ViewerSurface>\n      </ViewerBody>\n    </ViewerRoot>\n  );\n}\n\nfunction normalizeFileSystemLightPath(path: string) {\n  return path.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n}\n\nfunction normalizeOptionalFileSystemLightPath(path: string | null | undefined) {\n  return path ? normalizeFileSystemLightPath(path) : null;\n}\n\nfunction getFileSystemLightPathSegments(path: string | null) {\n  return path ? path.split(\"/\").filter(Boolean) : [\"/\"];\n}\n",
      "type": "registry:ui",
      "target": "@ui/file-system-light.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/file-system-light-tree.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { FileTree as PierreFileTree, useFileTree } from \"@pierre/trees/react\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport type FileSystemLightTreeProps = Omit<\n  React.ComponentProps<typeof PierreFileTree>,\n  \"model\"\n> & {\n  onSelectedPathsChange: (selectedPaths: string[]) => void;\n  paths: string[];\n  selectedPaths: string[];\n};\n\nexport function FileSystemLightTree({\n  onSelectedPathsChange,\n  paths,\n  selectedPaths,\n  ...props\n}: FileSystemLightTreeProps) {\n  const onSelectedPathsChangeRef = React.useRef(onSelectedPathsChange);\n\n  useKeyedMountEffect(joinEffectKey([onSelectedPathsChange]), () => {\n    onSelectedPathsChangeRef.current = onSelectedPathsChange;\n  });\n\n  const { model } = useFileTree({\n    flattenEmptyDirectories: false,\n    icons: { colored: false, set: \"complete\" },\n    initialExpansion: \"open\",\n    initialSelectedPaths: selectedPaths,\n    itemHeight: 32,\n    onSelectionChange: (nextSelectedPaths) => {\n      onSelectedPathsChangeRef.current([...nextSelectedPaths]);\n    },\n    overscan: 12,\n    paths,\n    search: false,\n    stickyFolders: false,\n  });\n\n  useKeyedMountEffect(joinEffectKey([model, paths]), () => {\n    model.resetPaths(paths);\n  });\n\n  useKeyedMountEffect(joinEffectKey([model, selectedPaths]), () => {\n    const selectedPathSet = new Set(selectedPaths);\n\n    for (const path of model.getSelectedPaths()) {\n      if (!selectedPathSet.has(path)) {\n        model.getItem(path)?.deselect();\n      }\n    }\n    for (const path of selectedPathSet) {\n      if (!model.getSelectedPaths().includes(path)) {\n        model.getItem(path)?.select();\n      }\n    }\n  });\n\n  return <PierreFileTree {...props} model={model} />;\n}\n",
      "type": "registry:ui",
      "target": "@ui/file-system-light-tree.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/viewer.tsx",
      "content": "\"use client\";\n\nexport { ViewerBody } from \"./viewer-body\";\nexport type { ViewerSidebarTriggerProps } from \"./viewer-chrome\";\nexport {\n  ViewerFrame,\n  ViewerHeader,\n  ViewerSidebarTrigger,\n} from \"./viewer-chrome\";\nexport {\n  ViewerRoot,\n  useOptionalViewerSidebar,\n  useViewerSidebar,\n} from \"./viewer-root\";\nexport { ViewerSidebar } from \"./viewer-sidebar\";\nexport { ViewerSurface, ViewerViewport } from \"./viewer-surface\";\nexport type {\n  ViewerBodyProps,\n  ViewerDataAttributes,\n  ViewerFrameProps,\n  ViewerHeaderProps,\n  ViewerRootProps,\n  ViewerSidebarCollapsible,\n  ViewerSidebarStateValue,\n  ViewerSidebarMode,\n  ViewerSidebarProps,\n  ViewerSidebarRequestedMode,\n  ViewerSidebarSide,\n  ViewerSidebarSlotNames,\n  ViewerSidebarState,\n  ViewerStateAttributeNamespace,\n  ViewerStateAttributeSlot,\n  ViewerStateAttributeValues,\n  ViewerSurfaceProps,\n  ViewerViewportProps,\n} from \"./viewer-types\";\n",
      "type": "registry:ui",
      "target": "@ui/viewer.tsx"
    }
  ],
  "type": "registry:ui"
}