{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "csv",
  "title": "CSV parser",
  "description": "A small, dependency-free RFC 4180-ish CSV/TSV parser returning a header and rectangular rows.",
  "files": [
    {
      "path": "registry/new-york-v4/lib/csv.ts",
      "content": "import {\n  createCsvNormalizer,\n  padRowsToColumnCount,\n  type CsvNormalizer,\n} from \"./csv-normalizer\";\nimport { createCsvParser } from \"./csv-parser\";\n\nexport {\n  DEFAULT_CSV_DIALECT,\n  extensionOfDelimitedName,\n  inferCsvDialect,\n  isTabDelimited,\n  normalizeCsvDelimiter,\n  resolveCsvDialect,\n  type CsvDialect,\n  type CsvDialectDescriptor,\n} from \"./csv-dialect\";\nexport {\n  createCsvNormalizer,\n  padRowsToColumnCount,\n  type CsvNormalizer,\n  type CsvNormalizerOptions,\n  type CsvTable,\n  type CsvTableEvent,\n} from \"./csv-normalizer\";\nexport {\n  createCsvParser,\n  type CsvParser,\n  type CsvParserOptions,\n} from \"./csv-parser\";\n\nexport interface ParsedCsv {\n  columns: string[];\n  rows: string[][];\n}\n\nexport interface ParseCsvOptions {\n  /** Field delimiter. Defaults to \",\". */\n  delimiter?: string;\n  /** Treat the first record as a header row. Defaults to true. */\n  hasHeader?: boolean;\n}\n\nexport function parseCsv(\n  input: string,\n  options: ParseCsvOptions = {},\n): ParsedCsv {\n  const parser = createCsvParser({ delimiter: options.delimiter });\n  const normalizer = createCsvNormalizer({ hasHeader: options.hasHeader });\n  const rows: string[][] = [];\n  let columns: string[] = [];\n\n  handleCsvRecords(parser.push(input), normalizer, rows, (nextColumns) => {\n    columns = nextColumns;\n    padRowsToColumnCount(rows, columns.length);\n  });\n  handleCsvRecords(parser.flush(), normalizer, rows, (nextColumns) => {\n    columns = nextColumns;\n    padRowsToColumnCount(rows, columns.length);\n  });\n\n  return { columns, rows };\n}\n\nfunction handleCsvRecords(\n  records: string[][],\n  normalizer: CsvNormalizer,\n  rows: string[][],\n  onColumns: (columns: string[]) => void,\n) {\n  for (const record of records) {\n    for (const event of normalizer.accept(record)) {\n      if (event.type === \"columns\") {\n        onColumns(event.columns);\n      } else {\n        rows.push(event.row);\n      }\n    }\n  }\n}\n\n/** Pad a record to a fixed width. */\nexport function fitRow(row: string[], width: number): string[] {\n  if (row.length === width) return row;\n  if (row.length < width)\n    return [...row, ...Array(width - row.length).fill(\"\")];\n  return row;\n}\n\nexport interface CsvStreamHandlers {\n  onColumns: (columns: string[]) => void;\n  onRows: (rows: string[][]) => void;\n  onDone?: () => void;\n  onError?: (error: unknown) => void;\n}\n\nexport interface CsvStreamOptions extends ParseCsvOptions {\n  /** Rows per batch handed to `onRows`. Defaults to 2000. */\n  batchSize?: number;\n  signal?: AbortSignal;\n}\n\nexport type CsvStreamSource =\n  | Blob\n  | string\n  | ReadableStream<Uint8Array>\n  | AsyncIterable<string>;\n\nexport async function streamCsv(\n  source: CsvStreamSource,\n  handlers: CsvStreamHandlers,\n  options: CsvStreamOptions = {},\n): Promise<void> {\n  const batchSize = options.batchSize ?? 2000;\n  const signal = options.signal;\n  const parser = createCsvParser({ delimiter: options.delimiter });\n  const normalizer = createCsvNormalizer({ hasHeader: options.hasHeader });\n  let batch: string[][] = [];\n\n  const handleRecords = (records: string[][]) => {\n    for (const record of records) {\n      for (const event of normalizer.accept(record)) {\n        if (event.type === \"columns\") {\n          handlers.onColumns(event.columns);\n          padRowsToColumnCount(batch, event.columns.length);\n        } else {\n          batch.push(event.row);\n        }\n      }\n      if (batch.length >= batchSize) {\n        handlers.onRows(batch);\n        batch = [];\n      }\n    }\n  };\n\n  try {\n    let lastYield = performance.now();\n    for await (const chunk of readTextChunks(source)) {\n      if (signal?.aborted) return;\n      handleRecords(parser.push(chunk));\n      if (performance.now() - lastYield > 12) {\n        await new Promise((resolve) => setTimeout(resolve));\n        lastYield = performance.now();\n      }\n    }\n    if (signal?.aborted) return;\n    handleRecords(parser.flush());\n    if (batch.length) handlers.onRows(batch);\n    handlers.onDone?.();\n  } catch (error) {\n    handlers.onError?.(error);\n  }\n}\n\nasync function* readTextChunks(\n  source: CsvStreamSource,\n): AsyncGenerator<string> {\n  if (typeof source === \"string\") {\n    const size = 1 << 20;\n    for (let offset = 0; offset < source.length; offset += size) {\n      yield source.slice(offset, offset + size);\n    }\n    return;\n  }\n  if (\n    typeof ReadableStream !== \"undefined\" &&\n    source instanceof ReadableStream\n  ) {\n    yield* decodeByteStream(source);\n    return;\n  }\n  if (Symbol.asyncIterator in Object(source)) {\n    for await (const chunk of source as AsyncIterable<string>) yield chunk;\n    return;\n  }\n  const blob = source as Blob;\n  if (typeof blob.stream === \"function\") {\n    yield* decodeByteStream(blob.stream());\n    return;\n  }\n  yield await blob.text();\n}\n\nasync function* decodeByteStream(\n  stream: ReadableStream<Uint8Array>,\n): AsyncGenerator<string> {\n  const reader = stream.getReader();\n  const decoder = new TextDecoder();\n  try {\n    while (true) {\n      const { done, value } = await reader.read();\n      if (done) break;\n      if (value) {\n        const text = decoder.decode(value, { stream: true });\n        if (text) yield text;\n      }\n    }\n    const rest = decoder.decode();\n    if (rest) yield rest;\n  } finally {\n    reader.releaseLock();\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/csv.ts"
    },
    {
      "path": "registry/new-york-v4/lib/csv-parser.ts",
      "content": "export interface CsvParser {\n  /** Feed a chunk of text; returns any records completed by it. */\n  push(text: string): string[][];\n  /** Emit the trailing record (call once at end of input). */\n  flush(): string[][];\n}\n\nexport interface CsvParserOptions {\n  /** Field delimiter. Defaults to \",\". */\n  delimiter?: string;\n}\n\n/**\n * Incremental CSV parser. Written in a deliberately plain, closure-free style\n * so it can run in both the main thread and a worker without adaptation.\n */\nexport function createCsvParser(options?: CsvParserOptions): CsvParser {\n  var delimiter = (options && options.delimiter) || \",\";\n  var record: string[] = [];\n  var field = \"\";\n  var inQuotes = false;\n  var pendingQuote = false;\n  var pendingCR = false;\n  var fieldStarted = false;\n  var sawFirstChar = false;\n\n  function push(text: string): string[][] {\n    var out: string[][] = [];\n    for (var i = 0; i < text.length; i++) {\n      var c = text.charAt(i);\n      if (!sawFirstChar) {\n        sawFirstChar = true;\n        if (c === \"\\uFEFF\") continue;\n      }\n\n      if (pendingCR) {\n        pendingCR = false;\n        record.push(field);\n        field = \"\";\n        out.push(record);\n        record = [];\n        fieldStarted = false;\n        if (c === \"\\n\") continue;\n      }\n\n      if (pendingQuote) {\n        pendingQuote = false;\n        if (c === '\"') {\n          field += '\"';\n          fieldStarted = true;\n          continue;\n        }\n        inQuotes = false;\n      }\n\n      if (inQuotes) {\n        if (c === '\"') pendingQuote = true;\n        else field += c;\n        continue;\n      }\n\n      if (c === '\"' && !fieldStarted) {\n        inQuotes = true;\n        fieldStarted = true;\n      } else if (c === delimiter) {\n        record.push(field);\n        field = \"\";\n        fieldStarted = false;\n      } else if (c === \"\\r\") {\n        pendingCR = true;\n      } else if (c === \"\\n\") {\n        record.push(field);\n        field = \"\";\n        out.push(record);\n        record = [];\n        fieldStarted = false;\n      } else {\n        field += c;\n        fieldStarted = true;\n      }\n    }\n    return out;\n  }\n\n  function flush(): string[][] {\n    var out: string[][] = [];\n    if (pendingQuote) {\n      pendingQuote = false;\n      inQuotes = false;\n    }\n    if (pendingCR) {\n      pendingCR = false;\n      record.push(field);\n      field = \"\";\n      out.push(record);\n      record = [];\n      fieldStarted = false;\n      return out;\n    }\n    if (field.length > 0 || record.length > 0 || fieldStarted) {\n      record.push(field);\n      field = \"\";\n      out.push(record);\n      record = [];\n      fieldStarted = false;\n    }\n    return out;\n  }\n\n  return { push: push, flush: flush };\n}\n",
      "type": "registry:lib",
      "target": "@lib/csv-parser.ts"
    },
    {
      "path": "registry/new-york-v4/lib/csv-normalizer.ts",
      "content": "export interface CsvTable {\n  columns: string[];\n  rows: string[][];\n}\n\nexport interface CsvNormalizerOptions {\n  /** Treat the first record as a header row. Defaults to true. */\n  hasHeader?: boolean;\n}\n\nexport type CsvTableEvent =\n  | { type: \"columns\"; columns: string[] }\n  | { type: \"row\"; row: string[] };\n\nexport interface CsvNormalizer {\n  accept(record: string[]): CsvTableEvent[];\n  getColumns(): string[];\n}\n\nexport function createCsvNormalizer(\n  options?: CsvNormalizerOptions,\n): CsvNormalizer {\n  var hasHeader = !options || options.hasHeader !== false;\n  var columns: string[] | null = null;\n  var width = 0;\n  var sawFirstRecord = false;\n\n  function makeColumnName(index: number): string {\n    return \"Column \" + (index + 1);\n  }\n\n  function copyAndPad(row: string[], nextWidth: number): string[] {\n    var out = row.slice();\n    while (out.length < nextWidth) out.push(\"\");\n    return out;\n  }\n\n  function growColumns(nextWidth: number): CsvTableEvent[] {\n    if (!columns || nextWidth <= width) return [];\n    var next = columns.slice();\n    for (var i = width; i < nextWidth; i++) {\n      next.push(hasHeader ? \"\" : makeColumnName(i));\n    }\n    columns = next;\n    width = nextWidth;\n    return [{ type: \"columns\", columns: next.slice() }];\n  }\n\n  function accept(record: string[]): CsvTableEvent[] {\n    if (!sawFirstRecord) {\n      sawFirstRecord = true;\n      width = record.length;\n      if (hasHeader) {\n        columns = record.slice();\n        return [{ type: \"columns\", columns: columns.slice() }];\n      }\n      columns = [];\n      for (var i = 0; i < width; i++) columns.push(makeColumnName(i));\n      return [\n        { type: \"columns\", columns: columns.slice() },\n        { type: \"row\", row: copyAndPad(record, width) },\n      ];\n    }\n\n    return growColumns(record.length).concat({\n      type: \"row\",\n      row: copyAndPad(record, width),\n    });\n  }\n\n  function getColumns(): string[] {\n    return columns ? columns.slice() : [];\n  }\n\n  return { accept: accept, getColumns: getColumns };\n}\n\nexport function padRowsToColumnCount(\n  rows: string[][],\n  columnCount: number,\n): void {\n  for (const row of rows) {\n    while (row.length < columnCount) row.push(\"\");\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/csv-normalizer.ts"
    },
    {
      "path": "registry/new-york-v4/lib/csv-dialect.ts",
      "content": "export interface CsvDialect {\n  delimiter: string;\n  hasHeader: boolean;\n}\n\nexport interface CsvDialectDescriptor {\n  src?: string;\n  fileName?: string;\n  mimeType?: string;\n}\n\nexport const DEFAULT_CSV_DIALECT: CsvDialect = {\n  delimiter: \",\",\n  hasHeader: true,\n};\n\nexport function normalizeCsvDelimiter(\n  delimiter: string | undefined,\n): string | undefined {\n  return delimiter === \"\\\\t\" ? \"\\t\" : delimiter;\n}\n\n/**\n * Normalizes a delimiter and discards values the parser cannot honor. A CSV\n * delimiter must be exactly one character: the incremental parser matches a\n * field separator one character at a time (`c === delimiter`), so an empty\n * delimiter makes it fall back to a comma (`delimiter || \",\"`) while a\n * multi-character delimiter (e.g. `\"::\"`) never matches at all and collapses\n * every row into a single field. In both cases the exporter would still join\n * with the unusable delimiter, producing output the parser cannot read back.\n * Callers must never let either through, so anything other than a single\n * character is discarded here and resolution falls back to the inferred dialect.\n */\nfunction usableDelimiter(delimiter: string | undefined): string | undefined {\n  const normalized = normalizeCsvDelimiter(delimiter);\n  return normalized && normalized.length === 1 ? normalized : undefined;\n}\n\nexport function extensionOfDelimitedName(name: string): string | null {\n  const clean = name.split(/[?#]/)[0];\n  const base = clean.split(\"/\").pop() ?? clean;\n  const dot = base.lastIndexOf(\".\");\n  return dot > 0 ? base.slice(dot + 1).toLowerCase() : null;\n}\n\nexport function inferCsvDialect({\n  src,\n  fileName,\n  mimeType,\n}: CsvDialectDescriptor): CsvDialect {\n  const name = fileName ?? src ?? \"\";\n  const ext = extensionOfDelimitedName(name);\n  if (ext === \"tsv\") return { delimiter: \"\\t\", hasHeader: true };\n  if (ext === \"csv\") return DEFAULT_CSV_DIALECT;\n\n  const normalizedMime = mimeType?.toLowerCase().split(\";\")[0].trim();\n  if (normalizedMime === \"text/tab-separated-values\") {\n    return { delimiter: \"\\t\", hasHeader: true };\n  }\n  return DEFAULT_CSV_DIALECT;\n}\n\nexport function resolveCsvDialect({\n  dialect,\n  delimiter,\n  hasHeader,\n  descriptor,\n}: {\n  dialect?: CsvDialect;\n  delimiter?: string;\n  hasHeader?: boolean;\n  descriptor: CsvDialectDescriptor;\n}): CsvDialect {\n  const inferred = dialect ?? inferCsvDialect(descriptor);\n  return {\n    delimiter:\n      usableDelimiter(delimiter) ??\n      usableDelimiter(inferred.delimiter) ??\n      DEFAULT_CSV_DIALECT.delimiter,\n    hasHeader: hasHeader ?? inferred.hasHeader,\n  };\n}\n\nexport function isTabDelimited(dialect: CsvDialect): boolean {\n  return normalizeCsvDelimiter(dialect.delimiter) === \"\\t\";\n}\n",
      "type": "registry:lib",
      "target": "@lib/csv-dialect.ts"
    }
  ],
  "type": "registry:lib"
}