{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "csv-import",
  "type": "registry:block",
  "title": "CSV import",
  "description": "Papa Parse bounded import, required headers, row validation, preview and guarded commit.",
  "dependencies": [
    "papaparse@^5.5.3",
    "@types/papaparse@^5.3.16"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "blocks/csv-import.tsx",
      "type": "registry:component",
      "target": "components/shine/csv-import.tsx",
      "content": "\"use client\";\nimport { useId,useRef,useState } from \"react\";\nimport Papa from \"papaparse\";\nimport { Button } from \"@/components/ui/button\";\nexport type ImportColumn={key:string;label:string;validate:(value:string,row:Record<string,string>)=>string|undefined};\nexport function CsvImport({columns,onImport,maxRows=1000,maxBytes=2000000}:{columns:ImportColumn[];onImport:(rows:Record<string,string>[])=>Promise<void>;maxRows?:number;maxBytes?:number}) {\n const id=useId(),lock=useRef(false),generation=useRef(0);const [rows,setRows]=useState<Record<string,string>[]>([]),[errors,setErrors]=useState<string[]>([]),[busy,setBusy]=useState(false),[status,setStatus]=useState(\"\");\n async function preview(file?:File){const request=++generation.current;setRows([]);setErrors([]);setStatus(\"\");if(!file)return;if(file.size>maxBytes){setErrors([\"File exceeds \"+maxBytes+\" bytes.\"]);return;}try{const text=await file.text();if(generation.current!==request)return;const parsed=Papa.parse<string[]>(text,{delimiter:\",\",skipEmptyLines:\"greedy\",preview:maxRows+2});const [header,...data]=parsed.data;const issues=parsed.errors.map(error=>\"CSV row \"+((error.row??0)+1)+\": \"+error.message);if(!header?.length)issues.push(\"The CSV needs a header row.\");if(header&&new Set(header).size!==header.length)issues.push(\"Column names must be unique.\");for(const column of columns)if(!header?.includes(column.key))issues.push(\"Missing column: \"+column.key);if(data.length>maxRows||parsed.meta.truncated)issues.push(\"Maximum \"+maxRows+\" records per import.\");const records=data.slice(0,maxRows).map((cells,index)=>{if(cells.length!==header.length)issues.push(\"Row \"+(index+2)+\": column count does not match the header.\");const row=Object.fromEntries((header||[]).map((key,i)=>[key,cells[i]||\"\"]));for(const column of columns){const message=column.validate(row[column.key]||\"\",row);if(message)issues.push(\"Row \"+(index+2)+\", \"+column.label+\": \"+message);}return row;});if(!records.length)issues.push(\"No records to import.\");setRows(records);setErrors(issues);setStatus(records.length+\" records parsed. \"+issues.length+\" issues.\");}catch(cause){if(generation.current===request)setErrors([cause instanceof Error?cause.message:\"Could not read CSV.\"]);}}\n return <section data-product-pattern=\"csv-import\" className=\"min-w-0 space-y-4 rounded-lg border border-border p-5\"><h2 className=\"text-lg font-semibold\">Import CSV</h2><p className=\"text-sm text-muted-foreground\">Required headers: {columns.map(column=>column.key).join(\", \")}. Maximum {maxRows} records and {maxBytes} bytes. Review before importing.</p><label htmlFor={id} className=\"block text-sm font-medium\">Choose CSV file</label><input id={id} type=\"file\" accept=\".csv,text/csv\" disabled={busy} onChange={event=>void preview(event.target.files?.[0])} className=\"block w-full min-w-0 text-sm\"/><p role=\"status\" className=\"text-sm\">{status}</p>{errors.length>0&&<div role=\"alert\" className=\"space-y-2 text-sm text-destructive\"><p>Resolve {errors.length} issues before importing.</p><ul className=\"max-h-48 list-inside list-disc overflow-y-auto\">{errors.map((error,index)=><li key={index}>{error}</li>)}</ul></div>}{rows.length>0&&<details open={rows.length<=10}><summary className=\"cursor-pointer py-3 text-sm font-medium\">Preview first {Math.min(10,rows.length)} of {rows.length} records</summary><div role=\"region\" aria-label=\"Import preview\" tabIndex={0} className=\"overflow-x-auto\"><table className=\"w-full text-left text-sm\"><caption className=\"sr-only\">CSV import preview</caption><thead><tr>{columns.map(column=><th scope=\"col\" key={column.key} className=\"border-b border-border p-3\">{column.label}</th>)}</tr></thead><tbody>{rows.slice(0,10).map((row,index)=><tr key={index}>{columns.map(column=><td key={column.key} className=\"border-b border-border p-3\">{row[column.key]}</td>)}</tr>)}</tbody></table></div></details>}<Button disabled={busy||!rows.length||errors.length>0} onClick={async()=>{if(lock.current)return;lock.current=true;setBusy(true);setStatus(\"\");try{await onImport(rows);setRows([]);setStatus(\"Import complete.\");}catch(cause){setStatus(cause instanceof Error?cause.message:\"Import failed. Preview retained; retry when ready.\");}finally{lock.current=false;setBusy(false);}}}>{busy?\"Importing…\":\"Confirm import\"}</Button></section>;\n}\n"
    }
  ]
}
