{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-grid",
  "title": "Data Grid",
  "author": "AlgorizeTech",
  "description": "An editable spreadsheet-style grid with inline cell editing and keyboard navigation. Use it when users need to change values in place rather than just read them.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/DataGridBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Download } from \"lucide-react\";\nimport React, {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { getThemeStyles } from \"./data-grid-theme\";\n\n// ==========================================\n// THEME & CONFIGURATION\n// ==========================================\nexport type GridColor =\n  | \"indigo\"\n  | \"rose\"\n  | \"emerald\"\n  | \"blue\"\n  | \"zinc\"\n  | (string & {});\n\n// ==========================================\n// INTERFACES\n// ==========================================\nexport interface GridColumn {\n  field: string;\n  headerName: string;\n  width?: string | number;\n  type?: \"text\" | \"number\" | (string & {});\n  readOnly?: boolean;\n}\n\nexport interface DataGridBlockProps<\n  T extends object = Record<string, unknown>,\n> extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n  columns: GridColumn[];\n  data: T[];\n  color?: GridColor;\n  onChange?: (newData: T[]) => void;\n  showRowNumbers?: boolean;\n  readOnly?: boolean;\n  title?: string;\n  description?: string;\n  showExport?: boolean;\n  exportFileName?: string;\n  onExport?: (data: T[]) => void;\n}\n\n// ==========================================\n// COMPONENT\n// ==========================================\nconst DataGridBlockImpl = forwardRef<HTMLDivElement, DataGridBlockProps>(\n  (\n    {\n      columns,\n      data: initialData,\n      color = \"primary\",\n      onChange,\n      showRowNumbers = true,\n      readOnly = false,\n      title,\n      description,\n      showExport = true,\n      exportFileName,\n      onExport,\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const { theme, style } = getThemeStyles(color as string);\n    const containerRef = useRef<HTMLDivElement>(null);\n    const editInputRef = useRef<HTMLInputElement>(null);\n\n    // containerRef drives keyboard navigation, so the forwarded ref is\n    // mirrored onto it rather than replacing it.\n    React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);\n\n    // State\n    const [data, setData] = useState(initialData);\n    const [activeCell, setActiveCell] = useState<{\n      row: number;\n      col: number;\n    } | null>(null);\n    const [editingCell, setEditingCell] = useState<{\n      row: number;\n      col: number;\n    } | null>(null);\n    const [editValue, setEditValue] = useState<string>(\"\");\n\n    // Sync external data changes\n    useEffect(() => {\n      setData(initialData);\n    }, [initialData]);\n\n    const finishEditing = useCallback(\n      (save: boolean = true) => {\n        if (editingCell && save && !readOnly) {\n          const colDef = columns[editingCell.col];\n          if (!colDef.readOnly) {\n            const newData = [...data];\n            const parsedValue =\n              colDef.type === \"number\" ? Number(editValue) || 0 : editValue;\n            newData[editingCell.row] = {\n              ...newData[editingCell.row],\n              [colDef.field]: parsedValue,\n            };\n            setData(newData);\n            if (onChange) onChange(newData);\n          }\n        }\n        setEditingCell(null);\n      },\n      [data, columns, editingCell, editValue, onChange, readOnly],\n    );\n\n    const startEditing = useCallback(\n      (row: number, col: number, initialValue?: string) => {\n        if (readOnly || columns[col].readOnly) return;\n        setEditingCell({ row, col });\n        setEditValue(\n          initialValue !== undefined\n            ? initialValue\n            : String(data[row][columns[col].field] || \"\"),\n        );\n      },\n      [columns, data, readOnly],\n    );\n\n    // Handle Global Grid Navigation\n    const handleGridKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n      if (editingCell || !activeCell) return; // Ignore if editing or no selection\n\n      const { row, col } = activeCell;\n      const rowCount = data.length;\n      const colCount = columns.length;\n\n      let nextRow = row;\n      let nextCol = col;\n      let handled = true;\n\n      switch (e.key) {\n        case \"ArrowUp\":\n          nextRow = Math.max(0, row - 1);\n          break;\n        case \"ArrowDown\":\n          nextRow = Math.min(rowCount - 1, row + 1);\n          break;\n        case \"ArrowLeft\":\n          nextCol = Math.max(0, col - 1);\n          break;\n        case \"ArrowRight\":\n          nextCol = Math.min(colCount - 1, col + 1);\n          break;\n        case \"Tab\":\n          e.preventDefault();\n          if (e.shiftKey) {\n            if (col > 0) nextCol = col - 1;\n            else if (row > 0) {\n              nextRow = row - 1;\n              nextCol = colCount - 1;\n            }\n          } else {\n            if (col < colCount - 1) nextCol = col + 1;\n            else if (row < rowCount - 1) {\n              nextRow = row + 1;\n              nextCol = 0;\n            }\n          }\n          break;\n        case \"Enter\":\n        case \"F2\":\n          startEditing(row, col);\n          e.preventDefault();\n          break;\n        case \"Escape\":\n          setActiveCell(null);\n          break;\n        case \"Backspace\":\n        case \"Delete\":\n          if (!readOnly && !columns[col].readOnly) {\n            const newData = [...data];\n            newData[row] = { ...newData[row], [columns[col].field]: \"\" };\n            setData(newData);\n            if (onChange) onChange(newData);\n          }\n          break;\n        default:\n          if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {\n            startEditing(row, col, e.key);\n            e.preventDefault();\n          } else {\n            handled = false;\n          }\n          break;\n      }\n\n      if (handled) {\n        if (nextRow !== row || nextCol !== col) {\n          e.preventDefault();\n          setActiveCell({ row: nextRow, col: nextCol });\n          const cellId = `cell-${nextRow}-${nextCol}`;\n          document.getElementById(cellId)?.scrollIntoView({\n            behavior: \"auto\",\n            block: \"nearest\",\n            inline: \"nearest\",\n          });\n        }\n      }\n    };\n\n    // Handle Input specifics while editing\n    const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        e.preventDefault();\n        finishEditing(true);\n        if (activeCell && activeCell.row < data.length - 1) {\n          setActiveCell({ row: activeCell.row + 1, col: activeCell.col });\n        }\n        containerRef.current?.focus({ preventScroll: true });\n      } else if (e.key === \"Escape\") {\n        e.preventDefault();\n        finishEditing(false); // Cancel\n        containerRef.current?.focus({ preventScroll: true });\n      } else if (e.key === \"Tab\") {\n        e.preventDefault();\n        finishEditing(true);\n        if (activeCell && activeCell.col < columns.length - 1) {\n          setActiveCell({ row: activeCell.row, col: activeCell.col + 1 });\n        }\n        containerRef.current?.focus({ preventScroll: true });\n      }\n      e.stopPropagation(); // Prevent grid listener from firing\n    };\n\n    // CSV Export Handler\n    const handleExportCSV = useCallback(() => {\n      if (!data || data.length === 0) return;\n\n      const headerRow = columns\n        .map((col) => `\"${col.headerName.replace(/\"/g, '\"\"')}\"`)\n        .join(\",\");\n      const rows = data.map((row) =>\n        columns\n          .map((col) => {\n            const val = (row as Record<string, unknown>)[col.field];\n            if (val === null || val === undefined) return '\"\"';\n            const str = String(val).replace(/\"/g, '\"\"');\n            return `\"${str}\"`;\n          })\n          .join(\",\"),\n      );\n\n      const csvContent = [headerRow, ...rows].join(\"\\r\\n\");\n      const blob = new Blob([csvContent], { type: \"text/csv;charset=utf-8;\" });\n      const url = URL.createObjectURL(blob);\n      const link = document.createElement(\"a\");\n      const defaultFileName = title\n        ? `${title.toLowerCase().replace(/[^a-z0-9]+/g, \"-\")}.csv`\n        : \"data-grid.csv\";\n      link.setAttribute(\"href\", url);\n      link.setAttribute(\"download\", exportFileName || defaultFileName);\n      link.style.visibility = \"hidden\";\n      document.body.appendChild(link);\n      link.click();\n      document.body.removeChild(link);\n      URL.revokeObjectURL(url);\n\n      if (onExport) {\n        onExport(data as unknown as Record<string, unknown>[]);\n      }\n    }, [columns, data, title, exportFileName, onExport]);\n\n    // Click outside to deselect\n    useEffect(() => {\n      const handleClickOutside = (e: MouseEvent) => {\n        if (\n          containerRef.current &&\n          !containerRef.current.contains(e.target as Node)\n        ) {\n          setActiveCell(null);\n          finishEditing(true);\n        }\n      };\n      document.addEventListener(\"mousedown\", handleClickOutside);\n      return () =>\n        document.removeEventListener(\"mousedown\", handleClickOutside);\n    }, [finishEditing]);\n\n    const hasHeader = Boolean(title || description || (showExport && title));\n\n    return (\n      <div\n        className={cn(\"w-full flex flex-col gap-2\", className)}\n        style={style}\n        {...props}\n      >\n        {hasHeader && (\n          <div className=\"flex items-center justify-between gap-3 px-1 mb-1\">\n            <div className=\"flex flex-col min-w-0\">\n              {title && (\n                <h3 className=\"font-semibold text-lg text-foreground truncate\">\n                  {title}\n                </h3>\n              )}\n              {description ? (\n                <p className=\"text-sm text-muted-foreground\">{description}</p>\n              ) : (\n                <p className=\"text-sm text-muted-foreground\">\n                  {readOnly\n                    ? \"View mode only.\"\n                    : \"Double-click any cell or press Enter to edit.\"}\n                </p>\n              )}\n            </div>\n            {showExport && (\n              <button\n                type=\"button\"\n                onClick={handleExportCSV}\n                style={\n                  color && String(color).startsWith(\"#\")\n                    ? { backgroundColor: String(color), color: \"#fff\" }\n                    : undefined\n                }\n                className={cn(\n                  \"flex items-center gap-2 px-3 py-1.5 text-xs font-semibold rounded-lg transition-all shadow-xs shrink-0 cursor-pointer\",\n                  color === \"indigo\"\n                    ? \"bg-indigo-600 hover:bg-indigo-700 text-white\"\n                    : color === \"rose\"\n                    ? \"bg-rose-600 hover:bg-rose-700 text-white\"\n                    : color === \"emerald\"\n                    ? \"bg-emerald-600 hover:bg-emerald-700 text-white\"\n                    : color === \"blue\"\n                    ? \"bg-blue-600 hover:bg-blue-700 text-white\"\n                    : color === \"lavender\"\n                    ? \"bg-[#9b8afb] hover:bg-[#9b8afb]/90 text-white\"\n                    : \"bg-primary text-primary-foreground hover:opacity-90\",\n                )}\n              >\n                <Download className=\"w-3.5 h-3.5 shrink-0\" />\n                <span>Export CSV</span>\n              </button>\n            )}\n          </div>\n        )}\n\n        <div className=\"w-full overflow-hidden border border-border rounded-xl bg-card text-card-foreground shadow-sm\">\n          <div\n            ref={containerRef}\n            tabIndex={0}\n            onKeyDown={handleGridKeyDown}\n            className=\"w-full overflow-auto max-h-[600px] outline-none\"\n          >\n          <table className=\"w-full border-collapse text-sm text-left whitespace-nowrap select-none\">\n            {/* HEADERS */}\n            <thead\n              className={cn(\n                \"sticky top-0 z-20 border-b border-border shadow-xs backdrop-blur-md transition-colors duration-150\",\n                theme.headerBg,\n              )}\n            >\n              <tr>\n                {showRowNumbers && (\n                  <th className=\"w-12 px-4 py-2 border-r border-border/60 text-center font-medium opacity-70\">\n                    #\n                  </th>\n                )}\n                {columns.map((col, idx) => (\n                  <th\n                    key={idx}\n                    className=\"px-4 py-2.5 font-semibold border-r border-border/60 last:border-r-0\"\n                    style={{ minWidth: col.width || 120 }}\n                  >\n                    {col.headerName}\n                  </th>\n                ))}\n              </tr>\n            </thead>\n\n            {/* BODY */}\n            <tbody>\n              {data.map((row, rowIndex) => (\n                <tr\n                  key={rowIndex}\n                  className=\"border-b border-border hover:bg-muted/50 transition-colors\"\n                >\n                  {showRowNumbers && (\n                    <td className=\"w-12 px-4 py-2 text-center text-xs font-mono text-muted-foreground border-r border-border bg-muted\">\n                      {rowIndex + 1}\n                    </td>\n                  )}\n\n                  {columns.map((col, colIndex) => {\n                    const isActive =\n                      activeCell?.row === rowIndex &&\n                      activeCell?.col === colIndex;\n                    const isEditingThis =\n                      editingCell?.row === rowIndex &&\n                      editingCell?.col === colIndex;\n                    const val = row[col.field];\n\n                    return (\n                      <td\n                        key={colIndex}\n                        id={`cell-${rowIndex}-${colIndex}`}\n                        onClick={() => {\n                          setActiveCell({ row: rowIndex, col: colIndex });\n                          containerRef.current?.focus({ preventScroll: true });\n                        }}\n                        onDoubleClick={() => startEditing(rowIndex, colIndex)}\n                        style={isActive ? style : undefined}\n                        className={cn(\n                          \"relative px-4 py-2 border-r border-border last:border-r-0 h-10 truncate transition-colors duration-75 cursor-cell\",\n                          isActive &&\n                            \"outline outline-2 -outline-offset-2 z-10 bg-card\",\n                          isActive && theme.outline,\n                          activeCell?.row === rowIndex &&\n                            !isActive &&\n                            \"bg-muted/80\",\n                          isEditingThis && \"p-0\",\n                        )}\n                      >\n                        {isEditingThis ? (\n                          <input\n                            ref={editInputRef}\n                            autoFocus\n                            type={col.type === \"number\" ? \"number\" : \"text\"}\n                            value={editValue}\n                            onChange={(e) => setEditValue(e.target.value)}\n                            onKeyDown={handleInputKeyDown}\n                            onBlur={() => finishEditing(true)}\n                            className={cn(\n                              \"absolute inset-0 w-full h-full px-4 outline-none bg-card font-sans text-sm m-0 border-none shadow-xl z-20\",\n                              col.type === \"number\" && \"font-mono\",\n                            )}\n                          />\n                        ) : (\n                          <span\n                            className={cn(\n                              \"block w-full overflow-hidden text-ellipsis\",\n                              col.type === \"number\" &&\n                                \"font-mono text-muted-foreground\",\n                              (readOnly || col.readOnly) &&\n                                \"opacity-60 cursor-not-allowed\",\n                            )}\n                          >\n                            {col.field === \"status\" ? (\n                              <span\n                                className={cn(\n                                  \"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold select-none\",\n                                  val === \"Paid\" &&\n                                    \"bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/20\",\n                                  val === \"Pending\" &&\n                                    \"bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20\",\n                                  val === \"Overdue\" &&\n                                    \"bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20\",\n                                  val !== \"Paid\" && val !== \"Pending\" && val !== \"Overdue\" && \"bg-muted text-foreground\"\n                                )}\n                              >\n                                {String(val || \"\")}\n                              </span>\n                            ) : (\n                              val !== null && val !== undefined ? String(val) : \"\"\n                            )}\n                          </span>\n                        )}\n\n                        {isActive && !isEditingThis && (\n                          <div\n                            className={cn(\n                              \"absolute bottom-[-3px] right-[-3px] w-1.5 h-1.5 z-20 border border-card rounded-[1px]\",\n                              theme.handle,\n                            )}\n                          />\n                        )}\n                      </td>\n                    );\n                  })}\n                </tr>\n              ))}\n            </tbody>\n          </table>\n\n          {data.length === 0 && (\n            <div className=\"py-12 text-center text-sm text-muted-foreground\">\n              No data available.\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n},\n);\nDataGridBlockImpl.displayName = \"DataGridBlock\";\n\n/**\n * Generic over the row shape, so `data` infers T and `onChange` accepts the\n * matching setState function without a cast:\n *\n *   const [rows, setRows] = useState(ROWS);\n *   <DataGridBlock columns={COLUMNS} data={rows} onChange={setRows} />\n */\nexport const DataGridBlock = DataGridBlockImpl as <\n  T extends object = Record<string, unknown>,\n>(\n  props: DataGridBlockProps<T> & React.RefAttributes<HTMLDivElement>,\n) => React.ReactElement | null;\n",
      "type": "registry:component",
      "target": "components/custom/blocks/DataGridBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/data-grid-theme.ts",
      "content": "\"use client\";\n\nimport React from \"react\";\n\nconst colorThemes: Record<string, Record<string, string>> = {\n  primary: {\n    outline: \"outline-[var(--primary)]\",\n    bgActive: \"bg-primary/10\",\n    handle: \"bg-primary\",\n    headerBg: \"bg-primary/10 dark:bg-primary/20 text-primary\",\n    btnBg: \"bg-primary text-primary-foreground hover:opacity-90\",\n  },\n  indigo: {\n    outline: \"outline-indigo-500\",\n    bgActive: \"bg-indigo-50 dark:bg-indigo-500/10\",\n    handle: \"bg-indigo-500\",\n    headerBg: \"bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300\",\n    btnBg: \"bg-indigo-600 hover:bg-indigo-700 text-white\",\n  },\n  rose: {\n    outline: \"outline-rose-500\",\n    bgActive: \"bg-rose-50 dark:bg-rose-500/10\",\n    handle: \"bg-rose-500\",\n    headerBg: \"bg-rose-50 dark:bg-rose-950/40 text-rose-700 dark:text-rose-300\",\n    btnBg: \"bg-rose-600 hover:bg-rose-700 text-white\",\n  },\n  emerald: {\n    outline: \"outline-emerald-500\",\n    bgActive: \"bg-emerald-50 dark:bg-emerald-500/10\",\n    handle: \"bg-emerald-500\",\n    headerBg: \"bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300\",\n    btnBg: \"bg-emerald-600 hover:bg-emerald-700 text-white\",\n  },\n  blue: {\n    outline: \"outline-blue-500\",\n    bgActive: \"bg-blue-50 dark:bg-blue-500/10\",\n    handle: \"bg-blue-500\",\n    headerBg: \"bg-blue-50 dark:bg-blue-950/40 text-blue-700 dark:text-blue-300\",\n    btnBg: \"bg-blue-600 hover:bg-blue-700 text-white\",\n  },\n  zinc: {\n    outline: \"outline-zinc-900 dark:outline-zinc-100\",\n    bgActive: \"bg-zinc-100 dark:bg-zinc-800\",\n    handle: \"bg-zinc-900 dark:bg-zinc-100\",\n    headerBg: \"bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100\",\n    btnBg: \"bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 hover:bg-zinc-800 dark:hover:bg-zinc-200\",\n  },\n};\n\nconst hexToRgb = (hex: string) => {\n  let cleanHex = hex.replace(/^#/, \"\");\n  if (cleanHex.length === 3)\n    cleanHex = cleanHex\n      .split(\"\")\n      .map((x) => x + x)\n      .join(\"\");\n  const result = /^([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(cleanHex);\n  return result\n    ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`\n    : \"99, 102, 241\";\n};\n\nexport const getThemeStyles = (color: string) => {\n  if (color && color.startsWith(\"#\")) {\n    const rgb = hexToRgb(color);\n    return {\n      theme: {\n        outline: \"\",\n        bgActive: \"bg-[rgba(var(--grid-color-rgb),0.1)]\",\n        handle: \"bg-[var(--grid-color)]\",\n        headerBg: \"bg-[rgba(var(--grid-color-rgb),0.12)] text-[var(--grid-color)]\",\n        btnBg: \"bg-[var(--grid-color)] text-white hover:opacity-90\",\n      },\n      style: {\n        \"--grid-color\": color,\n        \"--grid-color-rgb\": rgb,\n        outlineColor: color,\n      } as React.CSSProperties,\n    };\n  }\n  return { theme: colorThemes[color] || colorThemes.primary, style: {} };\n};\n",
      "type": "registry:component",
      "target": "components/custom/blocks/data-grid-theme.ts"
    }
  ],
  "docs": "Named exports: DataGridBlock, getThemeStyles (from data-grid-theme.ts, which installs alongside). `columns` take { field, headerName, width?, type?, readOnly? } — `type` number constrains input. Editing is local to the component, so persist changes through your own handler. Zero runtime dependencies.",
  "categories": [
    "data-display"
  ],
  "type": "registry:component"
}
