{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "csv-viewer",
  "title": "CSV Viewer",
  "description": "A sortable table for delimited data with a sticky header and a row cap.",
  "dependencies": [
    "lucide-react@^1.31.0",
    "papaparse@^5.6.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/csv-viewer/csv-viewer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDown, ArrowUp, TriangleAlert } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type CsvTable = {\n  fields: string[]\n  rows: string[][]\n}\n\nexport type CsvParser = (source: string | File) => Promise<CsvTable>\n\nexport type CsvViewerProps = Omit<\n  React.HTMLAttributes<HTMLElement>,\n  \"children\"\n> & {\n  source?: string | File\n  table?: CsvTable\n  parser?: CsvParser\n  maxRows?: number\n  label?: string\n  emptyLabel?: React.ReactNode\n  loadingLabel?: React.ReactNode\n}\n\ntype Sort = { column: number; direction: \"asc\" | \"desc\" } | null\n\n/** Hold the loading state back so a fast result never flashes it. */\nconst LOADING_DELAY_MS = 120\n\nconst MISSING_PAPAPARSE =\n  'CsvViewer needs the \"papaparse\" package, or a `parser` prop that returns { fields, rows }.'\n\nasync function parseWithPapa(source: string | File): Promise<CsvTable> {\n  let Papa: typeof import(\"papaparse\")\n\n  try {\n    Papa = await import(\"papaparse\")\n  } catch {\n    throw new Error(MISSING_PAPAPARSE)\n  }\n\n  // Bundlers disagree on whether this module arrives namespaced or default.\n  const parse = (Papa as { default?: typeof Papa }).default ?? Papa\n\n  return new Promise((resolve, reject) => {\n    parse.parse<string[]>(source as string, {\n      skipEmptyLines: true,\n      complete: (result: { data: string[][] }) => {\n        const [header = [], ...rows] = result.data\n        resolve({ fields: header, rows })\n      },\n      error: reject,\n    })\n  })\n}\n\nfunction compare(a: string, b: string) {\n  const left = Number(a)\n  const right = Number(b)\n\n  if (!Number.isNaN(left) && !Number.isNaN(right) && a !== \"\" && b !== \"\") {\n    return left - right\n  }\n\n  return a.localeCompare(b)\n}\n\nexport function CsvViewer({\n  source,\n  table,\n  parser = parseWithPapa,\n  maxRows = 200,\n  label = \"CSV contents\",\n  emptyLabel = \"This file has no rows.\",\n  loadingLabel = \"Reading the file…\",\n  className,\n  ...rootProps\n}: CsvViewerProps) {\n  const [fetched, setFetched] = React.useState<CsvTable | null>(null)\n  const [error, setError] = React.useState<string | null>(null)\n  const [loading, setLoading] = React.useState(false)\n  const [sort, setSort] = React.useState<Sort>(null)\n\n  const parsed = table ?? fetched\n\n  React.useEffect(() => {\n    if (table || source == null) return\n\n    const controller = new AbortController()\n    const spinner = window.setTimeout(() => setLoading(true), LOADING_DELAY_MS)\n\n    void parser(source)\n      .then((result) => {\n        if (controller.signal.aborted) return\n        setFetched(result)\n        setError(null)\n      })\n      .catch((cause: unknown) => {\n        if (controller.signal.aborted) return\n        setError(cause instanceof Error ? cause.message : String(cause))\n      })\n      .finally(() => {\n        window.clearTimeout(spinner)\n        if (!controller.signal.aborted) setLoading(false)\n      })\n\n    return () => {\n      controller.abort()\n      window.clearTimeout(spinner)\n    }\n  }, [source, table, parser])\n\n  const sorted = React.useMemo(() => {\n    if (!parsed) return null\n    if (!sort) return parsed.rows\n\n    const factor = sort.direction === \"asc\" ? 1 : -1\n\n    return [...parsed.rows].sort(\n      (a, b) => compare(a[sort.column] ?? \"\", b[sort.column] ?? \"\") * factor\n    )\n  }, [parsed, sort])\n\n  const visible = sorted?.slice(0, maxRows) ?? []\n  const hidden = (sorted?.length ?? 0) - visible.length\n\n  if (error) {\n    return (\n      <section\n        data-slot=\"csv-viewer\"\n        data-state=\"error\"\n        className={cn(\n          \"border-border bg-card text-destructive flex items-center gap-2 rounded-[var(--radius)] border px-4 py-3 text-sm\",\n          className\n        )}\n        {...rootProps}\n      >\n        <TriangleAlert aria-hidden=\"true\" size={15} className=\"shrink-0\" />\n        <p role=\"alert\">{error}</p>\n      </section>\n    )\n  }\n\n  return (\n    <section\n      data-slot=\"csv-viewer\"\n      data-state={loading ? \"loading\" : \"ready\"}\n      className={cn(\n        \"border-border bg-card text-card-foreground overflow-hidden rounded-[var(--radius)] border\",\n        className\n      )}\n      {...rootProps}\n    >\n      {loading && !parsed ? (\n        <p className=\"text-muted-foreground px-4 py-6 text-sm\">\n          {loadingLabel}\n        </p>\n      ) : null}\n\n      {parsed && parsed.rows.length === 0 ? (\n        <p className=\"text-muted-foreground px-4 py-6 text-sm\">{emptyLabel}</p>\n      ) : null}\n\n      {parsed && parsed.rows.length > 0 ? (\n        <>\n          <div className=\"max-h-[26rem] overflow-auto\">\n            <table className=\"w-full border-collapse text-sm\">\n              <caption className=\"sr-only\">{label}</caption>\n              <thead className=\"bg-muted sticky top-0 z-10\">\n                <tr>\n                  {parsed.fields.map((field, index) => {\n                    const active = sort?.column === index\n                    const direction = active ? sort.direction : undefined\n                    const Icon = direction === \"desc\" ? ArrowDown : ArrowUp\n\n                    return (\n                      <th\n                        key={`${field}-${index}`}\n                        scope=\"col\"\n                        aria-sort={\n                          active\n                            ? direction === \"asc\"\n                              ? \"ascending\"\n                              : \"descending\"\n                            : \"none\"\n                        }\n                        className=\"border-border border-b p-0 text-left font-semibold\"\n                      >\n                        <button\n                          type=\"button\"\n                          className=\"focus-visible:ring-ring hover:bg-background/60 flex min-h-11 w-full items-center gap-1.5 px-3 text-left transition-colors duration-150 focus-visible:ring-2 focus-visible:-outline-offset-2 focus-visible:outline-none motion-reduce:transition-none\"\n                          onClick={() =>\n                            setSort(\n                              active && direction === \"asc\"\n                                ? { column: index, direction: \"desc\" }\n                                : { column: index, direction: \"asc\" }\n                            )\n                          }\n                        >\n                          <span className=\"truncate\">{field}</span>\n                          {active ? (\n                            <Icon\n                              aria-hidden=\"true\"\n                              size={13}\n                              className=\"text-muted-foreground shrink-0\"\n                            />\n                          ) : null}\n                        </button>\n                      </th>\n                    )\n                  })}\n                </tr>\n              </thead>\n\n              <tbody>\n                {visible.map((row, rowIndex) => (\n                  <tr\n                    key={rowIndex}\n                    className=\"border-border hover:bg-muted/40 border-b transition-colors duration-150 last:border-b-0 motion-reduce:transition-none\"\n                  >\n                    {parsed.fields.map((_, columnIndex) => (\n                      <td\n                        key={columnIndex}\n                        className=\"max-w-[18rem] truncate px-3 py-2\"\n                      >\n                        {row[columnIndex] ?? \"\"}\n                      </td>\n                    ))}\n                  </tr>\n                ))}\n              </tbody>\n            </table>\n          </div>\n\n          <p\n            data-slot=\"csv-viewer-summary\"\n            className=\"text-muted-foreground border-border border-t px-3 py-2 text-xs\"\n          >\n            {hidden > 0\n              ? `Showing ${visible.length} of ${sorted?.length} rows`\n              : `${visible.length} ${visible.length === 1 ? \"row\" : \"rows\"}`}\n          </p>\n        </>\n      ) : null}\n    </section>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "document",
    "csv",
    "table",
    "data"
  ],
  "type": "registry:ui"
}