{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-system-block",
  "title": "File System",
  "description": "A document workspace over a flat object-store manifest with list, grid, columns, generated thumbnails, and persistent file previews.",
  "dependencies": [
    "lucide-react",
    "@pierre/trees"
  ],
  "registryDependencies": [
    "@retab/file-system"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/blocks/file-system-block.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { usePathname, useRouter, useSearchParams } from \"next/navigation\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { FileSystem } from \"@/components/ui/file-system\";\n\nimport {\n  collectFileSystemDemoFolderPaths,\n  collectFileSystemDemoItemPaths,\n  DEFAULT_FILE_SYSTEM_DEMO_QUERY,\n  FILE_SYSTEM_DEMO_ITEMS,\n  formatFileSystemDemoState,\n  LARGE_FILE_SYSTEM_DEMO_ITEMS,\n  parseFileSystemDemoState,\n  type FileSystemDemoState,\n} from \"./file-system-demo-state\";\n\nconst DEFAULT_FILE_SYSTEM_DEMO_STATE: FileSystemDemoState = {\n  path: \"\",\n  query: DEFAULT_FILE_SYSTEM_DEMO_QUERY,\n  selectedPath: null,\n  view: \"list\",\n};\n\nexport function FileSystemBlock() {\n  const router = useRouter();\n  const pathname = usePathname();\n  const searchParams = useSearchParams();\n  const isLarge = searchParams.get(\"large\") === \"true\";\n  const items = isLarge ? LARGE_FILE_SYSTEM_DEMO_ITEMS : FILE_SYSTEM_DEMO_ITEMS;\n  const folderPaths = React.useMemo(\n    () => collectFileSystemDemoFolderPaths(items),\n    [items],\n  );\n  const itemPaths = React.useMemo(\n    () => collectFileSystemDemoItemPaths(items),\n    [items],\n  );\n  const parsedState = React.useMemo(\n    () =>\n      parseFileSystemDemoState(searchParams, {\n        fallbackState: DEFAULT_FILE_SYSTEM_DEMO_STATE,\n        folderPaths,\n        itemPaths,\n      }),\n    [folderPaths, itemPaths, searchParams],\n  );\n  const [state, setState] = React.useState(parsedState);\n  const stateRef = React.useRef(parsedState);\n  const parsedStateKey = React.useMemo(\n    () =>\n      JSON.stringify(\n        formatFileSystemDemoState(parsedState, DEFAULT_FILE_SYSTEM_DEMO_STATE),\n      ),\n    [parsedState],\n  );\n\n  useKeyedMountEffect(parsedStateKey, () => {\n    stateRef.current = parsedState;\n    setState(parsedState);\n  });\n\n  const replaceState = React.useCallback(\n    (patch: Partial<FileSystemDemoState>) => {\n      const nextState = { ...stateRef.current, ...patch };\n      const nextParams = new URLSearchParams(\n        formatFileSystemDemoState(nextState, DEFAULT_FILE_SYSTEM_DEMO_STATE),\n      );\n\n      if (isLarge) nextParams.set(\"large\", \"true\");\n      stateRef.current = nextState;\n      setState(nextState);\n      router.replace(`${pathname}?${nextParams.toString()}`, {\n        scroll: false,\n      });\n    },\n    [isLarge, pathname, router],\n  );\n\n  return (\n    <FileSystem\n      className=\"h-[680px]\"\n      items={items}\n      onPathChange={(path) => replaceState({ path, selectedPath: null })}\n      onQueryChange={(query) => replaceState({ query })}\n      onSelectionChange={(entry) =>\n        replaceState({ selectedPath: entry?.path ?? null })\n      }\n      onViewChange={(view) => replaceState({ view })}\n      path={state.path}\n      query={state.query}\n      selectedPath={state.selectedPath}\n      view={state.view}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/file-system-block.tsx"
    },
    {
      "path": "registry/new-york-v4/blocks/file-system-demo-state.ts",
      "content": "import type {\n  FileSystemItem,\n  FileSystemQueryState,\n  FileSystemView,\n} from \"@/components/ui/file-system\";\n\nexport const DEFAULT_FILE_SYSTEM_DEMO_QUERY: FileSystemQueryState = {\n  search: \"\",\n  sort: { direction: \"asc\", key: \"name\" },\n};\n\nexport const FILE_SYSTEM_DEMO_ITEMS: FileSystemItem[] = [\n  {\n    kind: \"folder\",\n    path: \"financials/\",\n    updatedAt: \"2026-04-11T15:20:00Z\",\n  },\n  {\n    kind: \"folder\",\n    path: \"research/\",\n    updatedAt: \"2026-05-02T10:10:00Z\",\n  },\n  {\n    kind: \"folder\",\n    path: \"workspace/\",\n    updatedAt: \"2026-05-14T08:30:00Z\",\n  },\n  {\n    kind: \"file\",\n    path: \"financials/nvidia-financials-fy2024.xlsx\",\n    mimeType:\n      \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n    size: 231_482,\n    source: {\n      kind: \"url\",\n      url: \"/samples/nvidia-financials-fy2024.xlsx\",\n      fileName: \"nvidia-financials-fy2024.xlsx\",\n      mimeType:\n        \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n    },\n    updatedAt: \"2026-04-11T15:20:00Z\",\n  },\n  {\n    kind: \"file\",\n    path: \"financials/sales.csv\",\n    mimeType: \"text/csv\",\n    size: 18_420,\n    source: {\n      kind: \"url\",\n      url: \"/samples/sales.csv\",\n      fileName: \"sales.csv\",\n      mimeType: \"text/csv\",\n    },\n    updatedAt: \"2026-04-08T09:12:00Z\",\n  },\n  {\n    kind: \"file\",\n    path: \"research/attention.pdf\",\n    mimeType: \"application/pdf\",\n    previewImageUrl: \"/samples/attention-page-1.png\",\n    size: 516_280,\n    source: {\n      kind: \"url\",\n      url: \"/samples/attention.pdf\",\n      fileName: \"attention.pdf\",\n      mimeType: \"application/pdf\",\n    },\n    updatedAt: \"2026-05-02T10:10:00Z\",\n  },\n  {\n    kind: \"file\",\n    path: \"research/an-image-is-worth-16x16-words.pdf\",\n    mimeType: \"application/pdf\",\n    previewImageUrl: \"/samples/an-image-is-worth-16x16-words-page-1.png\",\n    size: 298_114,\n    source: {\n      kind: \"url\",\n      url: \"/samples/an-image-is-worth-16x16-words.pdf\",\n      fileName: \"an-image-is-worth-16x16-words.pdf\",\n      mimeType: \"application/pdf\",\n    },\n    updatedAt: \"2026-04-19T13:45:00Z\",\n  },\n  {\n    kind: \"file\",\n    path: \"workspace/quarterly-business-review.docx\",\n    mimeType:\n      \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n    size: 129_030,\n    source: {\n      kind: \"url\",\n      url: \"/samples/quarterly-business-review.docx\",\n      fileName: \"quarterly-business-review.docx\",\n      mimeType:\n        \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n    },\n    updatedAt: \"2026-05-14T08:30:00Z\",\n  },\n  {\n    kind: \"file\",\n    path: \"workspace/release-notes.md\",\n    mimeType: \"text/markdown\",\n    size: 4_812,\n    source: {\n      kind: \"url\",\n      url: \"/samples/release-notes.md\",\n      fileName: \"release-notes.md\",\n      mimeType: \"text/markdown\",\n    },\n    updatedAt: \"2026-05-10T17:05:00Z\",\n  },\n  {\n    kind: \"file\",\n    path: \"workspace/use-debounced-value.ts\",\n    mimeType: \"text/typescript\",\n    size: 2_190,\n    source: {\n      kind: \"url\",\n      url: \"/samples/use-debounced-value.ts\",\n      fileName: \"use-debounced-value.ts\",\n      mimeType: \"text/typescript\",\n    },\n    updatedAt: \"2026-05-09T11:15:00Z\",\n  },\n];\n\nexport const LARGE_FILE_SYSTEM_DEMO_ITEMS: FileSystemItem[] = [\n  {\n    kind: \"folder\",\n    path: \"large/\",\n    updatedAt: \"2026-06-01T00:00:00Z\",\n  },\n  ...Array.from({ length: 5_000 }, (_, index): FileSystemItem => {\n    const serial = index.toString().padStart(4, \"0\");\n\n    return {\n      kind: \"file\",\n      path: `large/file-${serial}.pdf`,\n      mimeType: \"application/pdf\",\n      size: index + 1,\n      source: {\n        kind: \"url\",\n        url: \"/samples/attention.pdf\",\n        fileName: `file-${serial}.pdf`,\n        mimeType: \"application/pdf\",\n      },\n      updatedAt: \"2026-06-01T00:00:00Z\",\n    };\n  }),\n];\n\nexport type FileSystemDemoState = {\n  path: string;\n  query: FileSystemQueryState;\n  selectedPath: string | null;\n  view: FileSystemView;\n};\n\nconst VIEWS: FileSystemView[] = [\"list\", \"grid\", \"columns\"];\nconst SORT_KEYS: FileSystemQueryState[\"sort\"][\"key\"][] = [\n  \"name\",\n  \"kind\",\n  \"size\",\n  \"updatedAt\",\n];\n\nexport function collectFileSystemDemoItemPaths(\n  items: readonly FileSystemItem[],\n) {\n  const paths = new Set<string>();\n\n  for (const item of items) {\n    paths.add(\n      item.kind === \"folder\" ? normalizeFolderPath(item.path) : item.path,\n    );\n  }\n\n  return paths;\n}\n\nexport function collectFileSystemDemoFolderPaths(\n  items: readonly FileSystemItem[],\n) {\n  const paths = new Set<string>([\"\"]);\n\n  for (const item of items) {\n    let path =\n      item.kind === \"folder\"\n        ? normalizeFolderPath(item.path)\n        : pathParent(item.path);\n\n    while (path) {\n      paths.add(path);\n      path = pathParent(path);\n    }\n  }\n\n  return paths;\n}\n\nexport function parseFileSystemDemoState(\n  searchParams: Pick<URLSearchParams, \"get\">,\n  {\n    fallbackState,\n    folderPaths,\n    itemPaths,\n  }: {\n    fallbackState: FileSystemDemoState;\n    folderPaths: ReadonlySet<string>;\n    itemPaths: ReadonlySet<string>;\n  },\n): FileSystemDemoState {\n  return {\n    path: parsePath(searchParams.get(\"path\"), folderPaths, fallbackState.path),\n    query: parseQuery(searchParams, fallbackState.query),\n    selectedPath: parseSelectedPath(\n      searchParams.get(\"selectedPath\"),\n      itemPaths,\n      fallbackState.selectedPath,\n    ),\n    view: parseView(searchParams.get(\"view\"), fallbackState.view),\n  };\n}\n\nexport function formatFileSystemDemoState(\n  state: FileSystemDemoState,\n  fallbackState: FileSystemDemoState,\n) {\n  const searchParams = new URLSearchParams();\n  const search = state.query.search.trim();\n\n  if (state.path !== fallbackState.path) searchParams.set(\"path\", state.path);\n  if (search !== fallbackState.query.search) searchParams.set(\"search\", search);\n  if (state.selectedPath && state.selectedPath !== fallbackState.selectedPath) {\n    searchParams.set(\"selectedPath\", state.selectedPath);\n  }\n  if (state.view !== fallbackState.view) searchParams.set(\"view\", state.view);\n  if (state.query.sort.key !== fallbackState.query.sort.key) {\n    searchParams.set(\"sort.key\", state.query.sort.key);\n  }\n  if (state.query.sort.direction !== fallbackState.query.sort.direction) {\n    searchParams.set(\"sort.direction\", state.query.sort.direction);\n  }\n\n  return searchParams.toString();\n}\n\nfunction parseQuery(\n  searchParams: Pick<URLSearchParams, \"get\">,\n  fallbackQuery: FileSystemQueryState,\n): FileSystemQueryState {\n  return {\n    search: (searchParams.get(\"search\") ?? fallbackQuery.search).trim(),\n    sort: {\n      direction: parseSortDirection(\n        searchParams.get(\"sort.direction\"),\n        fallbackQuery.sort.direction,\n      ),\n      key: parseSortKey(searchParams.get(\"sort.key\"), fallbackQuery.sort.key),\n    },\n  };\n}\n\nfunction parsePath(\n  value: string | null,\n  folderPaths: ReadonlySet<string>,\n  fallbackPath: string,\n) {\n  if (!value) return fallbackPath;\n  const path = normalizeFolderPath(value);\n\n  return folderPaths.has(path) ? path : fallbackPath;\n}\n\nfunction parseSelectedPath(\n  value: string | null,\n  itemPaths: ReadonlySet<string>,\n  fallbackSelectedPath: string | null,\n) {\n  return value && itemPaths.has(value) ? value : fallbackSelectedPath;\n}\n\nfunction parseView(value: string | null, fallbackView: FileSystemView) {\n  return VIEWS.includes(value as FileSystemView)\n    ? (value as FileSystemView)\n    : fallbackView;\n}\n\nfunction parseSortKey(\n  value: string | null,\n  fallbackSortKey: FileSystemQueryState[\"sort\"][\"key\"],\n) {\n  return SORT_KEYS.includes(value as FileSystemQueryState[\"sort\"][\"key\"])\n    ? (value as FileSystemQueryState[\"sort\"][\"key\"])\n    : fallbackSortKey;\n}\n\nfunction parseSortDirection(\n  value: string | null,\n  fallbackSortDirection: FileSystemQueryState[\"sort\"][\"direction\"],\n) {\n  return value === \"desc\" ? \"desc\" : fallbackSortDirection;\n}\n\nfunction normalizeFolderPath(path: string | undefined | null): string {\n  if (!path || path === \"/\") return \"\";\n  const trimmed = path.replace(/^\\/+/, \"\");\n  return trimmed.endsWith(\"/\") ? trimmed : `${trimmed}/`;\n}\n\nfunction pathParent(path: string): string {\n  const trimmed = path.endsWith(\"/\") ? path.slice(0, -1) : path;\n  const separatorIndex = trimmed.lastIndexOf(\"/\");\n\n  return separatorIndex === -1 ? \"\" : `${trimmed.slice(0, separatorIndex)}/`;\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/file-system-demo-state.ts"
    }
  ],
  "categories": [
    "primitives"
  ],
  "type": "registry:block"
}