{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["data","minimal"],"dependencies":["@ai-sdk/react","@base-ui/react","@loremllm/transport","@tanstack/react-table","@tanstack/react-virtual","cn","lucide-react","zod","zustand"],"description":"Spreadsheet data grid with AI enrichment — select cells, hit Enrich and each fills in after a Generating… beat; editable cells, drag/shift selection, arrow keys, resizable columns.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ChangeEvent, KeyboardEvent } from \"react\";\nimport { Input } from \"./ui\";\n\nimport type { SpreadsheetRow } from \"../lib/spreadsheet-store\";\nimport { useSpreadsheetStore } from \"../lib/spreadsheet-store\";\n\ninterface EditableCellProps {\n  row: { original: SpreadsheetRow };\n  column: { id: string };\n}\n\nexport const EditableCell = ({ row, column }: EditableCellProps) => {\n  const editingCell = useSpreadsheetStore((state) => state.editingCell);\n  const setEditingCell = useSpreadsheetStore((state) => state.setEditingCell);\n  const updateData = useSpreadsheetStore((state) => state.updateData);\n  // Cells are cleared to `undefined` by Delete/Backspace; coerce so <Input> stays controlled.\n  const value = row.original[column.id] ?? \"\";\n  const isEditing = editingCell?.rowId === row.original.id && editingCell?.columnId === column.id;\n\n  const onChange = (e: ChangeEvent<HTMLInputElement>) => {\n    updateData(row.original.id, column.id, e.target.value);\n  };\n\n  const onBlur = () => {\n    setEditingCell(null);\n  };\n\n  const onKeyDown = (e: KeyboardEvent) => {\n    if (e.key === \"Enter\" || e.key === \"Escape\") {\n      setEditingCell(null);\n    }\n    e.stopPropagation();\n  };\n\n  if (isEditing) {\n    return (\n      <Input\n        value={value}\n        onChange={onChange}\n        onBlur={onBlur}\n        onKeyDown={onKeyDown}\n        className=\"h-8 w-full rounded-none border-0 bg-transparent p-1 text-sm focus-visible:ring-blue-600/20 dark:focus-visible:ring-blue-400/30\"\n        autoFocus\n      />\n    );\n  }\n\n  return (\n    <div className=\"h-8 w-full p-1\">\n      <span className=\"text-(--foreground) block truncate text-sm\" title={value}>\n        {value}\n      </span>\n    </div>\n  );\n};\n","path":"/components/editable-cell.tsx","target":"uicapsule/spreadsheet/components/editable-cell.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport type { MouseEvent as ReactMouseEvent, RefObject, Ref } from \"react\";\nimport { cn } from \"cn\";\n\nimport { MIN_COLUMN_WIDTH } from \"../lib/spreadsheet-utils\";\n\n/** Half the handle's hit width, so the drag line sits centred on it. */\nconst DRAG_LINE_OFFSET = 2;\n\ninterface ResizeHandleProps {\n  columnId: string;\n  width: number;\n  handleColumnResize: (columnId: string, width: number) => void;\n  tableContainerRef: RefObject<HTMLDivElement | null>;\n  dragLineRef: RefObject<HTMLDivElement | null>;\n  setDragLineVisible: (visible: boolean) => void;\n  className?: string;\n  ref?: Ref<HTMLDivElement>;\n}\n\nexport const ResizeHandle = ({\n  columnId,\n  width,\n  handleColumnResize,\n  tableContainerRef,\n  dragLineRef,\n  setDragLineVisible,\n  className,\n  ref,\n}: ResizeHandleProps) => {\n  const [isResizing, setIsResizing] = useState(false);\n  const startXRef = useRef(0);\n  const startWidthRef = useRef(0);\n  const columnStartXRef = useRef(0);\n  const currentMouseXRef = useRef(0);\n\n  const handleMouseDown = (e: ReactMouseEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setIsResizing(true);\n    setDragLineVisible(true);\n    startXRef.current = e.clientX;\n    // Seed with the press position so a click with no drag commits the current width.\n    currentMouseXRef.current = e.clientX;\n    startWidthRef.current = width;\n\n    const headerElement = e.currentTarget.closest(\"[data-column-id]\");\n    if (headerElement instanceof HTMLElement) {\n      const rect = headerElement.getBoundingClientRect();\n      const tableRect = tableContainerRef.current?.getBoundingClientRect();\n      if (tableRect) {\n        columnStartXRef.current = rect.left - tableRect.left;\n        const initialDragX = columnStartXRef.current + startWidthRef.current - DRAG_LINE_OFFSET;\n        if (dragLineRef.current) {\n          dragLineRef.current.style.left = `${initialDragX}px`;\n        }\n      }\n    }\n\n    const widthForClientX = (clientX: number) =>\n      Math.max(MIN_COLUMN_WIDTH, startWidthRef.current + (clientX - startXRef.current));\n\n    const handleMouseMove = (event: MouseEvent) => {\n      currentMouseXRef.current = event.clientX;\n      const dragX = columnStartXRef.current + widthForClientX(event.clientX) - DRAG_LINE_OFFSET;\n\n      if (dragLineRef.current) {\n        dragLineRef.current.style.left = `${dragX}px`;\n      }\n    };\n\n    const handleMouseUp = () => {\n      setIsResizing(false);\n      setDragLineVisible(false);\n      handleColumnResize(columnId, widthForClientX(currentMouseXRef.current));\n      document.removeEventListener(\"mousemove\", handleMouseMove);\n      document.removeEventListener(\"mouseup\", handleMouseUp);\n    };\n\n    document.addEventListener(\"mousemove\", handleMouseMove);\n    document.addEventListener(\"mouseup\", handleMouseUp);\n  };\n\n  return (\n    <div\n      ref={ref}\n      role=\"presentation\"\n      className={cn(\n        \"hover:bg-(--primary)/50 dark:hover:bg-(--primary)/60 absolute top-0 right-0 h-full w-1 cursor-col-resize transition-colors\",\n        isResizing && \"bg-(--primary) dark:bg-(--primary)/80\",\n        className,\n      )}\n      onMouseDown={handleMouseDown}\n    />\n  );\n};\n","path":"/components/resize-handle.tsx","target":"uicapsule/spreadsheet/components/resize-handle.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport type { HTMLAttributes, ReactNode, Ref } from \"react\";\nimport { cn } from \"cn\";\nimport { flexRender, useTable } from \"@tanstack/react-table\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\n\nimport type { SpreadsheetRow } from \"../lib/spreadsheet-store\";\nimport type { ColumnInfo, NavigationMap, SpreadsheetFeatures } from \"../lib/spreadsheet-utils\";\nimport type { ColumnDef } from \"@tanstack/react-table\";\nimport { useSpreadsheetStore } from \"../lib/spreadsheet-store\";\nimport {\n  createNavigationMap,\n  getColumnSizeVars,\n  getRowCells,\n  MIN_COLUMN_WIDTH,\n  spreadsheetFeatures,\n} from \"../lib/spreadsheet-utils\";\nimport { useSpreadsheetHandlers } from \"../lib/use-spreadsheet-handlers\";\nimport { TableBody } from \"./table-body\";\nimport { ResizeHandle } from \"./resize-handle\";\n\nexport interface SpreadsheetProps extends HTMLAttributes<HTMLDivElement> {\n  columns: ColumnDef<SpreadsheetFeatures, SpreadsheetRow>[];\n  showRowNumbers?: boolean;\n  renderRowNumber?: (rowIndex: number) => ReactNode;\n  renderRowActions?: (row: SpreadsheetRow, rowIndex: number) => ReactNode;\n  ref?: Ref<HTMLDivElement>;\n}\n\nexport const Spreadsheet = ({\n  className,\n  style,\n  columns,\n  showRowNumbers = true,\n  renderRowNumber,\n  renderRowActions,\n  ref,\n  ...props\n}: SpreadsheetProps) => {\n  \"use no memo\";\n\n  // TanStack Virtual exposes mutable measurements that React Compiler cannot memoize.\n  const data = useSpreadsheetStore((state) => state.data);\n  const selectedCells = useSpreadsheetStore((state) => state.selectedCells);\n  const isDragging = useSpreadsheetStore((state) => state.dragStartCell !== null);\n  const columnWidths = useSpreadsheetStore((state) => state.columnWidths);\n  const setColumnWidths = useSpreadsheetStore((state) => state.setColumnWidths);\n  const dragLineVisible = useSpreadsheetStore((state) => state.dragLineVisible);\n  const setDragLineVisible = useSpreadsheetStore((state) => state.setDragLineVisible);\n\n  const tableContainerRef = useRef<HTMLDivElement>(null);\n  const dragLineRef = useRef<HTMLDivElement>(null);\n\n  const table = useTable({\n    columns,\n    data,\n    features: spreadsheetFeatures,\n  });\n\n  const leafColumns = table.getAllLeafColumns();\n  const columnMeta = useMemo<ColumnInfo[]>(\n    () => leafColumns.map((column) => ({ id: column.id })),\n    [leafColumns],\n  );\n\n  const navigationMap = useMemo<Map<string, NavigationMap>>(\n    () => createNavigationMap(data, columnMeta),\n    [data, columnMeta],\n  );\n\n  const columnSizeVars = useMemo(() => getColumnSizeVars(columnWidths), [columnWidths]);\n\n  const { handleMouseDown, handleMouseMove, handleMouseUp, handleKeyDown } = useSpreadsheetHandlers(\n    {\n      columns: columnMeta,\n      navigationMap,\n    },\n  );\n\n  const handleColumnResize = useCallback(\n    (columnId: string, width: number) => {\n      setColumnWidths((prev) => ({\n        ...prev,\n        [columnId]: Math.max(MIN_COLUMN_WIDTH, width),\n      }));\n    },\n    [setColumnWidths],\n  );\n\n  const getRowCellsHelper = useCallback(\n    (rowId: string) => getRowCells(rowId, columnMeta),\n    [columnMeta],\n  );\n\n  // Mouseup should be global to handle drag ending outside the spreadsheet\n  useEffect(() => {\n    document.addEventListener(\"mouseup\", handleMouseUp);\n    return () => {\n      document.removeEventListener(\"mouseup\", handleMouseUp);\n    };\n  }, [handleMouseUp]);\n\n  // oxlint-disable-next-line react/incompatible-library -- This component and its table body opt out of memoization for mutable virtualizer measurements.\n  const rowVirtualizer = useVirtualizer({\n    count: table.getRowModel().rows.length,\n    estimateSize: () => 36,\n    getScrollElement: () => tableContainerRef.current,\n    overscan: 10,\n  });\n\n  return (\n    <div\n      ref={ref}\n      role=\"grid\"\n      className={cn(\"relative flex flex-col\", isDragging && \"select-none\", className)}\n      style={{ ...columnSizeVars, ...style }}\n      tabIndex={0}\n      onKeyDown={handleKeyDown}\n      {...props}\n    >\n      <div\n        ref={dragLineRef}\n        className={cn(\n          \"bg-(--primary) pointer-events-none absolute top-0 bottom-0 z-50 w-0.5 transition-opacity\",\n          dragLineVisible ? \"opacity-100\" : \"opacity-0\",\n        )}\n      />\n\n      <div className=\"bg-(--muted) border-(--border) sticky top-0 z-1 flex border-y\">\n        {showRowNumbers && (\n          <div className=\"border-(--border) bg-(--muted) text-(--muted-foreground) flex h-10 w-12 shrink-0 items-center justify-center border-r text-xs font-medium\">\n            #\n          </div>\n        )}\n        {(table.getHeaderGroups()[0]?.headers ?? []).map((header) => (\n          <div\n            key={header.id}\n            role=\"presentation\"\n            data-column-id={header.column.id}\n            data-column-header\n            className=\"border-(--border) bg-(--muted) text-(--muted-foreground) relative flex h-10 shrink-0 cursor-default items-center border-r pl-1 text-left text-xs font-medium transition-colors\"\n            style={{\n              width: `calc(var(--col-${header.column.id}-size) * 1px)`,\n            }}\n            onMouseDown={(e) => handleMouseDown(e, \"\", header.column.id)}\n            onMouseMove={(e) => handleMouseMove(e, \"\", header.column.id)}\n          >\n            {header.isPlaceholder\n              ? null\n              : flexRender(header.column.columnDef.header, header.getContext())}\n            <ResizeHandle\n              columnId={header.column.id}\n              width={columnWidths[header.column.id] ?? 0}\n              handleColumnResize={handleColumnResize}\n              tableContainerRef={tableContainerRef}\n              dragLineRef={dragLineRef}\n              setDragLineVisible={setDragLineVisible}\n            />\n          </div>\n        ))}\n        {showRowNumbers && (\n          <div className=\"border-(--border) bg-(--muted) text-(--muted-foreground) flex h-10 w-12 shrink-0 items-center justify-center border-r text-xs font-medium\" />\n        )}\n      </div>\n\n      <div ref={tableContainerRef} className=\"flex-1\">\n        <div\n          style={{\n            height: `${rowVirtualizer.getTotalSize()}px`,\n            position: \"relative\",\n          }}\n        >\n          <TableBody\n            virtualItems={rowVirtualizer.getVirtualItems()}\n            table={table}\n            selectedCells={selectedCells}\n            getRowCells={getRowCellsHelper}\n            handleMouseDown={handleMouseDown}\n            handleMouseMove={handleMouseMove}\n            showRowNumbers={showRowNumbers}\n            renderRowNumber={renderRowNumber}\n            renderRowActions={renderRowActions}\n          />\n        </div>\n      </div>\n    </div>\n  );\n};\n","path":"/components/spreadsheet.tsx","target":"uicapsule/spreadsheet/components/spreadsheet.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"cn\";\n\nimport { useSpreadsheetStore } from \"../lib/spreadsheet-store\";\n\nexport type StatusBarProps = ComponentProps<\"div\">;\n\nexport const StatusBar = ({ className, ...props }: StatusBarProps) => (\n  <div\n    className={cn(\n      \"text-(--muted-foreground) bg-(--background) dark:bg-(--background)/95 sticky bottom-0 flex items-center justify-between border-t p-2 text-xs\",\n      className,\n    )}\n    {...props}\n  />\n);\n\nexport type StatusBarSectionProps = ComponentProps<\"div\">;\n\nexport const StatusBarSection = ({ className, ...props }: StatusBarSectionProps) => (\n  <div className={cn(\"flex flex-1 items-center gap-2\", className)} {...props} />\n);\n\nexport const StatusBarMessage = () => {\n  const editingCell = useSpreadsheetStore((state) => state.editingCell);\n  const selectedCells = useSpreadsheetStore((state) => state.selectedCells);\n  const data = useSpreadsheetStore((state) => state.data);\n  const selectedCellsCount = selectedCells.size;\n\n  if (editingCell) {\n    const rowIndex = data.findIndex((row) => row.id === editingCell.rowId);\n    const displayRowNumber = rowIndex === -1 ? \"?\" : rowIndex + 1;\n\n    return (\n      <span>\n        Editing row {displayRowNumber}, column {editingCell.columnId}\n      </span>\n    );\n  }\n\n  if (selectedCellsCount > 0) {\n    return (\n      <span>\n        Selected: {selectedCellsCount} cell\n        {selectedCellsCount > 1 ? \"s\" : \"\"}\n      </span>\n    );\n  }\n\n  return <span>Click a cell to select</span>;\n};\n\nexport const StatusBarSummary = () => {\n  const data = useSpreadsheetStore((state) => state.data);\n  // Rows carry an `id` alongside their cell values; only the latter are columns.\n  const columnCount = Object.keys(data[0] ?? {}).filter((key) => key !== \"id\").length;\n\n  return (\n    <span>\n      {data.length} rows × {columnCount} columns\n    </span>\n  );\n};\n","path":"/components/status-bar.tsx","target":"uicapsule/spreadsheet/components/status-bar.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport type { MouseEvent, ReactNode } from \"react\";\nimport { cn } from \"cn\";\nimport { flexRender } from \"@tanstack/react-table\";\n\nimport type { SpreadsheetRow } from \"../lib/spreadsheet-store\";\nimport type { SpreadsheetFeatures } from \"../lib/spreadsheet-utils\";\nimport type { Table } from \"@tanstack/react-table\";\nimport type { VirtualItem } from \"@tanstack/react-virtual\";\n\ninterface TableBodyProps {\n  virtualItems: VirtualItem[];\n  table: Table<SpreadsheetFeatures, SpreadsheetRow>;\n  selectedCells: Set<string>;\n  getRowCells: (rowId: string) => string[];\n  handleMouseDown: (e: MouseEvent, rowId: string, columnId: string) => void;\n  handleMouseMove: (e: MouseEvent, rowId: string, columnId: string) => void;\n  showRowNumbers?: boolean;\n  renderRowNumber?: (rowIndex: number) => ReactNode;\n  renderRowActions?: (row: SpreadsheetRow, rowIndex: number) => ReactNode;\n}\n\nexport const TableBody = ({\n  virtualItems,\n  table,\n  selectedCells,\n  getRowCells,\n  handleMouseDown,\n  handleMouseMove,\n  showRowNumbers = true,\n  renderRowNumber,\n  renderRowActions,\n}: TableBodyProps) => (\n  <>\n    {virtualItems.map((virtualRow) => {\n      const row = table.getRowModel().rows[virtualRow.index];\n      const rowIndex = virtualRow.index;\n      const rowId = row?.original?.id;\n      if (!rowId) {\n        return null;\n      }\n\n      const rowCells = getRowCells(rowId);\n      const isRowSelected = rowCells.every((cell) => selectedCells.has(cell));\n\n      return (\n        <div\n          key={row.id}\n          className={cn(\n            \"hover:bg-(--muted)/30 dark:hover:bg-(--muted)/20 absolute top-0 left-0 w-full transition-colors\",\n            isRowSelected && \"bg-(--muted)/50 dark:bg-(--muted)/40\",\n          )}\n          style={{\n            height: `${virtualRow.size}px`,\n            transform: `translateY(${virtualRow.start}px)`,\n          }}\n        >\n          <div className=\"flex h-full\">\n            {showRowNumbers && (\n              <div\n                role=\"presentation\"\n                data-row-number\n                className={cn(\n                  \"border-(--border) bg-(--muted)/30 text-(--muted-foreground) hover:bg-(--muted)/50 dark:hover:bg-(--muted)/40 flex h-9 w-12 shrink-0 cursor-default items-center justify-center border-r border-b font-mono text-xs transition-colors\",\n                  isRowSelected && \"bg-(--muted) dark:bg-(--muted)/80\",\n                )}\n                onMouseDown={(e) => handleMouseDown(e, rowId, \"\")}\n                onMouseMove={(e) => handleMouseMove(e, rowId, \"\")}\n              >\n                {renderRowNumber ? renderRowNumber(rowIndex) : rowIndex + 1}\n              </div>\n            )}\n            {row.getVisibleCells().map((cell) => {\n              const cellKey = `${rowId}:${cell.column.id}`;\n              const isCellSelected = selectedCells.has(cellKey);\n\n              return (\n                <div\n                  key={cell.id}\n                  role=\"presentation\"\n                  className={cn(\n                    \"border-(--border) relative flex h-9 shrink-0 cursor-default items-center border-r border-b transition-colors\",\n                    isCellSelected && \"bg-blue-50 dark:bg-blue-950/50\",\n                  )}\n                  style={{\n                    width: `calc(var(--col-${cell.column.id}-size) * 1px)`,\n                  }}\n                  onMouseDown={(e) => handleMouseDown(e, rowId, cell.column.id)}\n                  onMouseMove={(e) => handleMouseMove(e, rowId, cell.column.id)}\n                >\n                  {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                </div>\n              );\n            })}\n            {renderRowActions && (\n              <div className=\"border-(--border) flex h-9 shrink-0 items-center justify-center border-b\">\n                {renderRowActions(row.original, rowIndex)}\n              </div>\n            )}\n          </div>\n        </div>\n      );\n    })}\n  </>\n);\n","path":"/components/table-body.tsx","target":"uicapsule/spreadsheet/components/table-body.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { Button } from \"./ui\";\nimport { cn } from \"cn\";\n\nexport type ToolbarProps = ComponentProps<\"div\">;\n\nexport const Toolbar = ({ className, ...props }: ToolbarProps) => (\n  <div className={cn(\"flex items-center gap-2 p-2\", className)} {...props} />\n);\n\nexport type ToolbarButtonProps = ComponentProps<typeof Button>;\n\nexport const ToolbarButton = (props: ToolbarButtonProps) => (\n  <Button size=\"sm\" variant=\"outline\" {...props} />\n);\n","path":"/components/toolbar.tsx","target":"uicapsule/spreadsheet/components/toolbar.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { Menu } from \"@base-ui/react/menu\";\nimport { cn } from \"cn\";\nimport \"../spreadsheet.css\";\n\ntype Styled<Props> = Omit<Props, \"className\"> & { className?: string };\n\nexport const Button = ({\n  className,\n  variant = \"outline\",\n  size = \"sm\",\n  ...props\n}: Styled<ButtonPrimitive.Props> & {\n  variant?: \"outline\" | \"ghost\";\n  size?: \"sm\" | \"icon\";\n}) => (\n  <ButtonPrimitive\n    className={cn(\n      \"inline-flex shrink-0 items-center justify-center gap-2 rounded-full text-sm font-medium outline-none transition-colors hover:bg-(--muted) focus-visible:ring-2 focus-visible:ring-(--ring) disabled:opacity-50\",\n      variant === \"outline\" && \"border border-(--border) bg-(--background) shadow-xs\",\n      size === \"sm\" ? \"h-8 px-2.5\" : \"size-9\",\n      className,\n    )}\n    {...props}\n  />\n);\n\nexport const Input = ({ className, ...props }: ComponentProps<\"input\">) => (\n  <input\n    className={cn(\n      \"h-9 w-full rounded-md border border-(--input) bg-transparent px-2.5 py-1 text-sm outline-none focus-visible:ring-2 focus-visible:ring-(--ring)\",\n      className,\n    )}\n    {...props}\n  />\n);\n\nexport const DropdownMenu = Menu.Root;\nexport const DropdownMenuTrigger = Menu.Trigger;\n\nexport const DropdownMenuContent = ({\n  className,\n  align = \"start\",\n  ...props\n}: Styled<Menu.Popup.Props> & Pick<Menu.Positioner.Props, \"align\">) => (\n  <Menu.Portal>\n    <Menu.Positioner align={align} sideOffset={4} className=\"spreadsheet isolate z-50\">\n      <Menu.Popup\n        className={cn(\n          \"min-w-32 rounded-md bg-(--popover) p-1 text-(--popover-foreground) shadow-md ring-1 ring-(--foreground)/10 outline-none\",\n          className,\n        )}\n        {...props}\n      />\n    </Menu.Positioner>\n  </Menu.Portal>\n);\n\nexport const DropdownMenuItem = ({ className, ...props }: Styled<Menu.Item.Props>) => (\n  <Menu.Item\n    className={cn(\n      \"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none data-highlighted:bg-(--accent) data-disabled:opacity-50\",\n      className,\n    )}\n    {...props}\n  />\n);\n","path":"/components/ui.tsx","target":"uicapsule/spreadsheet/components/ui.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { createContext, use, useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createStore, useStore } from \"zustand\";\nimport \"../spreadsheet.css\";\n\n/** A cell holds the text typed into it, or `undefined` once cleared by Delete/Backspace. */\nexport type CellValue = string | undefined;\n\nexport interface SpreadsheetRow {\n  id: string;\n  [key: string]: CellValue;\n}\n\nexport interface SpreadsheetStore {\n  data: SpreadsheetRow[];\n\n  selectedCells: Set<string>;\n  setSelectedCells: (update: (prev: Set<string>) => Set<string>) => void;\n\n  editingCell: { rowId: string; columnId: string } | null;\n  setEditingCell: (cell: { rowId: string; columnId: string } | null) => void;\n\n  dragStartCell: { rowId: string; columnId: string } | null;\n  setDragStartCell: (cell: { rowId: string; columnId: string } | null) => void;\n\n  columnWidths: Record<string, number>;\n  setColumnWidths: (update: (prev: Record<string, number>) => Record<string, number>) => void;\n\n  dragLineVisible: boolean;\n  setDragLineVisible: (visible: boolean) => void;\n\n  updateData: (rowId: string, columnId: string, value: CellValue) => void;\n  updateSelectedCellsData: (value?: CellValue) => void;\n  addRow: (onCreateRow?: (rowIndex: number) => SpreadsheetRow) => void;\n  deleteRow: (rowId: string) => void;\n}\n\nconst createSpreadsheetStore = (\n  initialData: SpreadsheetRow[],\n  initialColumnWidths: Record<string, number>,\n) =>\n  createStore<SpreadsheetStore>((set, get) => ({\n    addRow: (onCreateRow) => {\n      set((state) => {\n        const nextRow = onCreateRow?.(state.data.length) ?? { id: crypto.randomUUID() };\n        return { data: [...state.data, nextRow] };\n      });\n    },\n    columnWidths: initialColumnWidths,\n    data: initialData,\n    deleteRow: (rowId) => {\n      set((state) => ({\n        data: state.data.filter((row) => row.id !== rowId),\n        dragStartCell: state.dragStartCell?.rowId === rowId ? null : state.dragStartCell,\n        editingCell: state.editingCell?.rowId === rowId ? null : state.editingCell,\n        selectedCells: new Set(\n          [...state.selectedCells].filter((key) => !key.startsWith(`${rowId}:`)),\n        ),\n      }));\n    },\n    dragLineVisible: false,\n    dragStartCell: null,\n    editingCell: null,\n    selectedCells: new Set(),\n    setColumnWidths: (widths) => {\n      set((state) => ({\n        columnWidths: widths(state.columnWidths),\n      }));\n    },\n    setDragLineVisible: (visible) => set({ dragLineVisible: visible }),\n    setDragStartCell: (cell) => set({ dragStartCell: cell }),\n    setEditingCell: (cell) => set({ editingCell: cell }),\n    setSelectedCells: (cells) => {\n      set((state) => ({\n        selectedCells: cells(state.selectedCells),\n      }));\n    },\n    updateData: (rowId, columnId, value) => {\n      set((state) => ({\n        data: state.data.map((row) => {\n          if (row.id === rowId) {\n            return { ...row, [columnId]: value };\n          }\n          return row;\n        }),\n      }));\n    },\n    updateSelectedCellsData: (value) => {\n      if (get().selectedCells.size === 0) {\n        return;\n      }\n\n      set((state) => {\n        const newData = [...state.data];\n        for (const cellKey of state.selectedCells) {\n          const [rowId, columnId] = cellKey.split(\":\");\n          if (!rowId || !columnId) {\n            continue;\n          }\n          const rowIndex = newData.findIndex((row) => row.id === rowId);\n          const currentRow = newData[rowIndex];\n          if (!currentRow) {\n            continue;\n          }\n\n          newData[rowIndex] = { ...currentRow, [columnId]: value };\n        }\n        return { data: newData };\n      });\n    },\n  }));\n\ntype SpreadsheetStoreApi = ReturnType<typeof createSpreadsheetStore>;\nconst SpreadsheetContext = createContext<SpreadsheetStoreApi | null>(null);\n\nexport const SpreadsheetProvider = ({\n  initialData,\n  initialColumnWidths,\n  children,\n}: {\n  initialData: SpreadsheetRow[];\n  initialColumnWidths: Record<string, number>;\n  children: ReactNode;\n}) => {\n  const store = useMemo(\n    () => createSpreadsheetStore(initialData, initialColumnWidths),\n    [initialData, initialColumnWidths],\n  );\n  return (\n    <SpreadsheetContext value={store}>\n      <div className=\"spreadsheet\">{children}</div>\n    </SpreadsheetContext>\n  );\n};\n\nexport const useSpreadsheetApi = () => {\n  const store = use(SpreadsheetContext);\n  if (!store) {\n    throw new Error(\"Spreadsheet components require SpreadsheetProvider\");\n  }\n  return store;\n};\n\nexport const useSpreadsheetStore = <T,>(selector: (state: SpreadsheetStore) => T): T =>\n  useStore(useSpreadsheetApi(), selector);\n","path":"/lib/spreadsheet-store.tsx","target":"uicapsule/spreadsheet/lib/spreadsheet-store.tsx","type":"registry:file"},{"content":"import { columnVisibilityFeature, tableFeatures } from \"@tanstack/react-table\";\n\nimport type { SpreadsheetRow } from \"./spreadsheet-store\";\n\n/**\n * Features every spreadsheet table is built on. Features are opt-in in\n * react-table v9, and the body renders through `row.getVisibleCells()`, which\n * column visibility provides. Declared here rather than in `spreadsheet.tsx` so\n * the body and cell components can type against it without an import cycle.\n */\nexport const spreadsheetFeatures = tableFeatures({ columnVisibilityFeature });\n\nexport type SpreadsheetFeatures = typeof spreadsheetFeatures;\n\n/** Columns never resize below this, both while dragging and once committed. */\nexport const MIN_COLUMN_WIDTH = 60;\n\nexport interface CellPosition {\n  rowId: string;\n  columnId: string;\n}\n\nexport interface ColumnInfo {\n  id: string;\n}\n\nexport interface NavigationMap {\n  up: string | null;\n  down: string | null;\n  left: string | null;\n  right: string | null;\n  tab: string | null;\n}\n\nexport type NavigationDirection = keyof NavigationMap;\n\nexport const getRowCells = (rowId: string, columns: ColumnInfo[]): string[] =>\n  columns.map((col) => `${rowId}:${col.id}`);\n\nexport const getColumnCells = (columnId: string, data: SpreadsheetRow[]): string[] =>\n  data.map((row) => `${row.id}:${columnId}`);\n\nexport const getRangeCells = (\n  startRowId: string,\n  startColId: string,\n  endRowId: string,\n  endColId: string,\n  columns: ColumnInfo[],\n  data: SpreadsheetRow[],\n): string[] => {\n  const columnIds = columns.map((col) => col.id);\n  const startColIndex = columnIds.indexOf(startColId);\n  const endColIndex = columnIds.indexOf(endColId);\n\n  const minColIndex = Math.min(startColIndex, endColIndex);\n  const maxColIndex = Math.max(startColIndex, endColIndex);\n\n  const startRowIndex = data.findIndex((row) => row.id === startRowId);\n  const endRowIndex = data.findIndex((row) => row.id === endRowId);\n\n  if (startRowIndex === -1 || endRowIndex === -1 || startColIndex === -1 || endColIndex === -1) {\n    return [];\n  }\n\n  const minRowIndex = Math.min(startRowIndex, endRowIndex);\n  const maxRowIndex = Math.max(startRowIndex, endRowIndex);\n\n  const cells: string[] = [];\n  for (let rowIndex = minRowIndex; rowIndex <= maxRowIndex; rowIndex += 1) {\n    const row = data[rowIndex];\n    if (!row) {\n      continue;\n    }\n    for (let colIndex = minColIndex; colIndex <= maxColIndex; colIndex += 1) {\n      const colId = columnIds[colIndex];\n      if (!colId) {\n        continue;\n      }\n      cells.push(`${row.id}:${colId}`);\n    }\n  }\n  return cells;\n};\n\nexport const toggleRowSelection = (\n  rowId: string,\n  selectedCells: Set<string>,\n  columns: ColumnInfo[],\n): Set<string> => {\n  const rowCells = getRowCells(rowId, columns);\n  const isRowFullySelected = rowCells.every((cell) => selectedCells.has(cell));\n\n  const newSelectedCells = new Set(selectedCells);\n  for (const cell of rowCells) {\n    if (isRowFullySelected) {\n      newSelectedCells.delete(cell);\n    } else {\n      newSelectedCells.add(cell);\n    }\n  }\n  return newSelectedCells;\n};\n\nexport const toggleColumnSelection = (\n  columnId: string,\n  selectedCells: Set<string>,\n  data: SpreadsheetRow[],\n): Set<string> => {\n  const columnCells = getColumnCells(columnId, data);\n  const isColumnFullySelected = columnCells.every((cell) => selectedCells.has(cell));\n\n  const newSelectedCells = new Set(selectedCells);\n  for (const cell of columnCells) {\n    if (isColumnFullySelected) {\n      newSelectedCells.delete(cell);\n    } else {\n      newSelectedCells.add(cell);\n    }\n  }\n  return newSelectedCells;\n};\n\nexport const toggleCellSelection = (cellKey: string, selectedCells: Set<string>): Set<string> => {\n  const newSelectedCells = new Set(selectedCells);\n  if (newSelectedCells.has(cellKey)) {\n    newSelectedCells.delete(cellKey);\n  } else {\n    newSelectedCells.add(cellKey);\n  }\n  return newSelectedCells;\n};\n\nexport const getFirstSelectedCell = (selectedCells: Set<string>): CellPosition | null => {\n  const [firstSelectedCell] = [...selectedCells];\n  if (!firstSelectedCell) {\n    return null;\n  }\n\n  const [rowId, columnId] = firstSelectedCell.split(\":\");\n  if (!rowId || !columnId) {\n    return null;\n  }\n  return { columnId, rowId };\n};\n\nexport const getColumnSizeVars = (columnWidths: Record<string, number>) => {\n  const colSizes: { [key: string]: number } = {};\n  for (const [columnId, width] of Object.entries(columnWidths)) {\n    colSizes[`--col-${columnId}-size`] = width;\n  }\n  return colSizes;\n};\n\nexport const isWithinDataAttribute = (target: EventTarget | null, attribute: string): boolean =>\n  target instanceof HTMLElement && !!target.closest(`[data-${attribute}]`);\n\nexport const shouldAllowEditing = (selectedCells: Set<string>, cellKey: string): boolean =>\n  selectedCells.has(cellKey) && selectedCells.size === 1;\n\nexport const createNavigationMap = (\n  data: SpreadsheetRow[],\n  columns: ColumnInfo[],\n): Map<string, NavigationMap> => {\n  const navigationMap = new Map<string, NavigationMap>();\n  const columnIds = columns.map((col) => col.id);\n\n  for (const [rowIndex, row] of data.entries()) {\n    for (const [colIndex, columnId] of columnIds.entries()) {\n      const cellKey = `${row.id}:${columnId}`;\n      const prevRowId = data[rowIndex - 1]?.id;\n      const nextRowId = data[rowIndex + 1]?.id;\n      const prevColId = columnIds[colIndex - 1];\n      const nextColId = columnIds[colIndex + 1];\n      const [firstColId] = columnIds;\n\n      const navigation: NavigationMap = {\n        down: nextRowId ? `${nextRowId}:${columnId}` : null,\n        left: prevColId ? `${row.id}:${prevColId}` : null,\n        right: nextColId ? `${row.id}:${nextColId}` : null,\n        tab: null,\n        up: prevRowId ? `${prevRowId}:${columnId}` : null,\n      };\n\n      if (nextColId) {\n        navigation.tab = `${row.id}:${nextColId}`;\n      } else if (nextRowId && firstColId) {\n        navigation.tab = `${nextRowId}:${firstColId}`;\n      }\n\n      navigationMap.set(cellKey, navigation);\n    }\n  }\n\n  return navigationMap;\n};\n\nexport const getNextCellPositionFromMap = (\n  cellKey: string,\n  direction: NavigationDirection,\n  navigationMap: Map<string, NavigationMap>,\n): CellPosition | null => {\n  const navigation = navigationMap.get(cellKey);\n  if (!navigation) {\n    return null;\n  }\n\n  const nextCellKey = navigation[direction];\n  if (!nextCellKey) {\n    return null;\n  }\n\n  const [rowId, columnId] = nextCellKey.split(\":\");\n  if (!rowId || !columnId) {\n    return null;\n  }\n  return { columnId, rowId };\n};\n","path":"/lib/spreadsheet-utils.ts","target":"uicapsule/spreadsheet/lib/spreadsheet-utils.ts","type":"registry:file"},{"content":"import { useCallback, useMemo } from \"react\";\nimport { useChat } from \"@ai-sdk/react\";\nimport { StaticChatTransport } from \"@loremllm/transport\";\n\nimport type { StoreApi } from \"zustand\";\nimport type { SpreadsheetStore } from \"./spreadsheet-store\";\nimport type { UIMessage } from \"@ai-sdk/react\";\nimport { useSpreadsheetApi } from \"./spreadsheet-store\";\n\ntype CustomUIMessage = UIMessage<\n  unknown,\n  {\n    updateCell: {\n      rowId: string;\n      columnId: string;\n      value: string;\n    };\n  }\n>;\n\n// The random index is always in range; the `?? list[0]` only satisfies\n// `noUncheckedIndexedAccess`, which the non-empty tuple keeps well-typed.\nconst pick = <T>(list: readonly [T, ...T[]]): T =>\n  list[Math.floor(Math.random() * list.length)] ?? list[0];\n\nconst fakeData = new Map<string, () => string>([\n  [\n    \"firstName\",\n    () => pick([\"John\", \"Jane\", \"Michael\", \"Sarah\", \"David\", \"Emily\", \"James\", \"Emma\"]),\n  ],\n  [\n    \"lastName\",\n    () => pick([\"Smith\", \"Johnson\", \"Williams\", \"Brown\", \"Jones\", \"Garcia\", \"Miller\", \"Davis\"]),\n  ],\n  [\n    \"email\",\n    () => {\n      const domain = pick([\"example.com\", \"demo.org\", \"test.io\"]);\n      return `${Math.random().toString(36).slice(2, 15)}@${domain}`;\n    },\n  ],\n  [\n    \"company\",\n    () =>\n      pick([\n        \"Acme Corp\",\n        \"Tech Solutions\",\n        \"Global Industries\",\n        \"Digital Ventures\",\n        \"Innovation Labs\",\n        \"Future Systems\",\n      ]),\n  ],\n  [\n    \"role\",\n    () =>\n      pick([\n        \"Software Engineer\",\n        \"Product Manager\",\n        \"Data Scientist\",\n        \"Designer\",\n        \"Marketing Director\",\n        \"Sales Executive\",\n      ]),\n  ],\n]);\n\nconst createTransport = (storeApi: StoreApi<SpreadsheetStore>) =>\n  new StaticChatTransport<CustomUIMessage>({\n    chunkDelayMs: [400, 1400],\n    async *mockResponse() {\n      const store = storeApi.getState();\n      const { selectedCells } = store;\n      const cells = [...selectedCells];\n\n      for (const cellKey of cells) {\n        const [rowId, columnId] = cellKey.split(\":\");\n        if (!rowId || !columnId) {\n          continue;\n        }\n        const generateValue = fakeData.get(columnId);\n        if (!generateValue) {\n          continue;\n        }\n        const value = generateValue();\n        store.updateData(rowId, columnId, \"Generating...\");\n\n        yield {\n          data: { columnId, rowId, value },\n          type: \"data-updateCell\",\n        };\n      }\n    },\n  });\n\nexport const useAiEnrichment = () => {\n  const storeApi = useSpreadsheetApi();\n  const transport = useMemo(() => createTransport(storeApi), [storeApi]);\n\n  const { sendMessage, status } = useChat<CustomUIMessage>({\n    onData: (dataPart) => {\n      if (dataPart.type === \"data-updateCell\") {\n        const { rowId, columnId, value } = dataPart.data;\n        const store = storeApi.getState();\n        store.updateData(rowId, columnId, value);\n      }\n    },\n    transport,\n  });\n\n  const handleEnrich = useCallback(() => {\n    void sendMessage({ text: \"enrich\" });\n  }, [sendMessage]);\n\n  const aiGenerating = status === \"streaming\" || status === \"submitted\";\n\n  return {\n    aiGenerating,\n    handleEnrich,\n  };\n};\n","path":"/lib/use-ai-enrichment.ts","target":"uicapsule/spreadsheet/lib/use-ai-enrichment.ts","type":"registry:file"},{"content":"import { useCallback } from \"react\";\nimport type { KeyboardEvent, MouseEvent } from \"react\";\nimport type { ColumnInfo, NavigationDirection, NavigationMap } from \"./spreadsheet-utils\";\nimport { useSpreadsheetStore } from \"./spreadsheet-store\";\nimport {\n  getColumnCells,\n  getFirstSelectedCell,\n  getNextCellPositionFromMap,\n  getRangeCells,\n  getRowCells,\n  isWithinDataAttribute,\n  shouldAllowEditing,\n  toggleCellSelection,\n  toggleColumnSelection,\n  toggleRowSelection,\n} from \"./spreadsheet-utils\";\n\ninterface UseSpreadsheetHandlersProps {\n  columns: ColumnInfo[];\n  navigationMap: Map<string, NavigationMap>;\n}\n\n/** Keys that move the single-cell selection, mapped to their direction in the navigation map. */\nconst NAVIGATION_KEYS = new Map<string, NavigationDirection>([\n  [\"ArrowUp\", \"up\"],\n  [\"ArrowDown\", \"down\"],\n  [\"ArrowLeft\", \"left\"],\n  [\"ArrowRight\", \"right\"],\n  [\"Tab\", \"tab\"],\n]);\n\nexport const useSpreadsheetHandlers = ({ columns, navigationMap }: UseSpreadsheetHandlersProps) => {\n  const data = useSpreadsheetStore((state) => state.data);\n  const selectedCells = useSpreadsheetStore((state) => state.selectedCells);\n  const setSelectedCells = useSpreadsheetStore((state) => state.setSelectedCells);\n  const editingCell = useSpreadsheetStore((state) => state.editingCell);\n  const setEditingCell = useSpreadsheetStore((state) => state.setEditingCell);\n  const dragStartCell = useSpreadsheetStore((state) => state.dragStartCell);\n  const setDragStartCell = useSpreadsheetStore((state) => state.setDragStartCell);\n  const updateSelectedCellsData = useSpreadsheetStore((state) => state.updateSelectedCellsData);\n\n  /** A multi-cell selection has no single edit target, so editing is cancelled. Returns `selection` for chaining. */\n  const exitEditIfMultiple = useCallback(\n    (selection: Set<string>) => {\n      if (selection.size > 1 && editingCell) {\n        setEditingCell(null);\n      }\n      return selection;\n    },\n    [editingCell, setEditingCell],\n  );\n\n  const handleMouseDown = useCallback(\n    (e: MouseEvent, rowId: string, columnId: string) => {\n      if (e.button !== 0) {\n        return;\n      }\n\n      const cellKey = `${rowId}:${columnId}`;\n\n      if (isWithinDataAttribute(e.target, \"row-number\")) {\n        if (e.ctrlKey || e.metaKey) {\n          setSelectedCells((currentSelectedCells) =>\n            exitEditIfMultiple(toggleRowSelection(rowId, currentSelectedCells, columns)),\n          );\n        } else {\n          setSelectedCells(() => exitEditIfMultiple(new Set(getRowCells(rowId, columns))));\n        }\n        return;\n      }\n\n      if (isWithinDataAttribute(e.target, \"column-header\")) {\n        if (e.ctrlKey || e.metaKey) {\n          setSelectedCells((currentSelectedCells) =>\n            exitEditIfMultiple(toggleColumnSelection(columnId, currentSelectedCells, data)),\n          );\n        } else {\n          setSelectedCells(() => exitEditIfMultiple(new Set(getColumnCells(columnId, data))));\n        }\n        return;\n      }\n\n      if (e.ctrlKey || e.metaKey) {\n        setSelectedCells((currentSelectedCells) =>\n          exitEditIfMultiple(toggleCellSelection(cellKey, currentSelectedCells)),\n        );\n      } else if (e.shiftKey) {\n        setSelectedCells((currentSelectedCells) => {\n          if (currentSelectedCells.size === 0) {\n            return new Set([cellKey]);\n          }\n\n          const [firstSelectedCell] = [...currentSelectedCells];\n          if (!firstSelectedCell) {\n            return new Set([cellKey]);\n          }\n          const [firstRowId, firstCol] = firstSelectedCell.split(\":\");\n          if (!firstRowId || !firstCol) {\n            return new Set([cellKey]);\n          }\n          const rangeCells = getRangeCells(firstRowId, firstCol, rowId, columnId, columns, data);\n          return exitEditIfMultiple(new Set(rangeCells));\n        });\n      } else {\n        const isCurrentlyEditing =\n          editingCell?.rowId === rowId && editingCell?.columnId === columnId;\n        const isCurrentlySelected = selectedCells.has(cellKey) && selectedCells.size === 1;\n\n        if (isCurrentlyEditing) {\n          setTimeout(() => setEditingCell(null), 0);\n        } else if (isCurrentlySelected) {\n          setTimeout(() => setEditingCell({ columnId, rowId }), 0);\n        } else {\n          setSelectedCells(() => new Set([cellKey]));\n        }\n      }\n\n      setDragStartCell({ columnId, rowId });\n    },\n    [\n      selectedCells,\n      columns,\n      data,\n      editingCell,\n      exitEditIfMultiple,\n      setSelectedCells,\n      setEditingCell,\n      setDragStartCell,\n    ],\n  );\n\n  const handleMouseMove = useCallback(\n    (e: MouseEvent, rowId: string, columnId: string) => {\n      if (!dragStartCell) {\n        return;\n      }\n\n      const rangeCells = getRangeCells(\n        dragStartCell.rowId,\n        dragStartCell.columnId,\n        rowId,\n        columnId,\n        columns,\n        data,\n      );\n      setSelectedCells(() => exitEditIfMultiple(new Set(rangeCells)));\n    },\n    [dragStartCell, columns, data, exitEditIfMultiple, setSelectedCells],\n  );\n\n  const handleMouseUp = useCallback(() => {\n    setDragStartCell(null);\n  }, [setDragStartCell]);\n\n  const handleKeyDown = useCallback(\n    (e: KeyboardEvent) => {\n      const firstSelectedCell = getFirstSelectedCell(selectedCells);\n      if (!firstSelectedCell) {\n        return;\n      }\n\n      const { rowId, columnId } = firstSelectedCell;\n\n      const { activeElement } = document;\n      if (activeElement && activeElement.tagName === \"INPUT\") {\n        return;\n      }\n\n      const direction = NAVIGATION_KEYS.get(e.key);\n      if (direction) {\n        e.preventDefault();\n        const nextPosition = getNextCellPositionFromMap(\n          `${rowId}:${columnId}`,\n          direction,\n          navigationMap,\n        );\n        if (nextPosition) {\n          setSelectedCells(() => new Set([`${nextPosition.rowId}:${nextPosition.columnId}`]));\n        }\n        return;\n      }\n\n      switch (e.key) {\n        case \"Enter\": {\n          e.preventDefault();\n          if (editingCell?.rowId === rowId && editingCell?.columnId === columnId) {\n            setEditingCell(null);\n          } else if (shouldAllowEditing(selectedCells, `${rowId}:${columnId}`)) {\n            setEditingCell({ columnId, rowId });\n          }\n          break;\n        }\n        case \"Escape\": {\n          e.preventDefault();\n          if (editingCell) {\n            setEditingCell(null);\n          } else {\n            setSelectedCells(() => new Set());\n          }\n          break;\n        }\n        case \"Delete\":\n        case \"Backspace\": {\n          e.preventDefault();\n          if (selectedCells.size > 0) {\n            updateSelectedCellsData();\n          }\n          break;\n        }\n        default: {\n          break;\n        }\n      }\n    },\n    [\n      selectedCells,\n      editingCell,\n      setEditingCell,\n      setSelectedCells,\n      updateSelectedCellsData,\n      navigationMap,\n    ],\n  );\n\n  return {\n    handleKeyDown,\n    handleMouseDown,\n    handleMouseMove,\n    handleMouseUp,\n  };\n};\n","path":"/lib/use-spreadsheet-handlers.ts","target":"uicapsule/spreadsheet/lib/use-spreadsheet-handlers.ts","type":"registry:file"},{"content":"\"use client\";\n\nimport {\n  Button,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"./components/ui\";\n\nimport { MoreVerticalIcon, Plus, Sparkles, TrashIcon } from \"lucide-react\";\n\nimport type { SpreadsheetProps } from \"./components/spreadsheet\";\nimport { EditableCell } from \"./components/editable-cell\";\nimport { Spreadsheet } from \"./components/spreadsheet\";\nimport {\n  StatusBar,\n  StatusBarMessage,\n  StatusBarSection,\n  StatusBarSummary,\n} from \"./components/status-bar\";\nimport { Toolbar, ToolbarButton } from \"./components/toolbar\";\nimport type { SpreadsheetRow } from \"./lib/spreadsheet-store\";\nimport { SpreadsheetProvider, useSpreadsheetStore } from \"./lib/spreadsheet-store\";\nimport { useAiEnrichment } from \"./lib/use-ai-enrichment\";\n\ninterface Person extends SpreadsheetRow {\n  id: string;\n  linkedinUrl: string;\n  firstName: string;\n  lastName: string;\n  email: string;\n  company: string;\n  role: string;\n}\n\nconst generateSamplePeople = (count: number): Person[] =>\n  Array.from({ length: count }, (_, i) => ({\n    company: \"\",\n    email: \"\",\n    firstName: \"\",\n    id: `${i + 1}`,\n    lastName: \"\",\n    linkedinUrl: `https://linkedin.com/in/user-${i + 1}`,\n    role: \"\",\n  }));\n\nconst initialData = generateSamplePeople(30);\nconst initialColumnWidths = {\n  company: 150,\n  email: 180,\n  firstName: 120,\n  lastName: 120,\n  linkedinUrl: 250,\n  role: 150,\n};\n\nconst RowActions = ({ row }: { row: SpreadsheetRow }) => {\n  const deleteRow = useSpreadsheetStore((state) => state.deleteRow);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        aria-label=\"Row actions\"\n        render={<Button size=\"icon\" variant=\"ghost\" className=\"h-8 w-8\" />}\n      >\n        <MoreVerticalIcon className=\"h-3 w-3\" />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem onClick={() => deleteRow(row.id)} className=\"text-(--destructive)\">\n          <TrashIcon className=\"size-4\" />\n          Delete\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nconst ToolbarButtons = () => {\n  const { handleEnrich } = useAiEnrichment();\n  const addRow = useSpreadsheetStore((state) => state.addRow);\n\n  return (\n    <>\n      <ToolbarButton\n        onClick={() =>\n          addRow(() => ({\n            company: \"\",\n            email: \"\",\n            firstName: \"\",\n            id: crypto.randomUUID(),\n            lastName: \"\",\n            linkedinUrl: \"\",\n            role: \"\",\n          }))\n        }\n      >\n        <Plus className=\"size-4\" />\n        Add Row\n      </ToolbarButton>\n      <ToolbarButton onClick={handleEnrich}>\n        <Sparkles className=\"size-4\" />\n        Enrich\n      </ToolbarButton>\n    </>\n  );\n};\n\nconst columns: SpreadsheetProps[\"columns\"] = [\n  { accessorKey: \"linkedinUrl\", cell: EditableCell, header: \"LinkedIn URL\" },\n  { accessorKey: \"firstName\", cell: EditableCell, header: \"First Name\" },\n  { accessorKey: \"lastName\", cell: EditableCell, header: \"Last Name\" },\n  { accessorKey: \"email\", cell: EditableCell, header: \"Email\" },\n  { accessorKey: \"company\", cell: EditableCell, header: \"Company\" },\n  { accessorKey: \"role\", cell: EditableCell, header: \"Role\" },\n];\n\nconst Preview = () => (\n  <SpreadsheetProvider initialData={initialData} initialColumnWidths={initialColumnWidths}>\n    <Toolbar>\n      <ToolbarButtons />\n    </Toolbar>\n    <Spreadsheet\n      columns={columns}\n      showRowNumbers\n      renderRowActions={(row) => <RowActions row={row} />}\n    />\n    <StatusBar>\n      <StatusBarSection>\n        <StatusBarMessage />\n      </StatusBarSection>\n      <StatusBarSection className=\"justify-end\">\n        <StatusBarSummary />\n      </StatusBarSection>\n    </StatusBar>\n  </SpreadsheetProvider>\n);\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/spreadsheet/preview.tsx","type":"registry:file"},{"content":".spreadsheet {\n  --background: #fff;\n  --foreground: #18181b;\n  --primary: #f66b2c;\n  --primary-foreground: #fff;\n  --secondary: #f4f4f5;\n  --secondary-foreground: #27272a;\n  --muted: #f4f4f5;\n  --muted-foreground: #71717a;\n  --accent: #f4f4f5;\n  --accent-foreground: #27272a;\n  --popover: #fff;\n  --popover-foreground: #18181b;\n  --border: #e4e4e7;\n  --input: #e4e4e7;\n  --ring: #a1a1aa;\n  --destructive: #dc2626;\n  color: var(--foreground);\n}\n\n.dark .spreadsheet {\n  --background: #18181b;\n  --foreground: #fafafa;\n  --primary-foreground: #27272a;\n  --secondary: #27272a;\n  --secondary-foreground: #fafafa;\n  --muted: #27272a;\n  --muted-foreground: #a1a1aa;\n  --accent: #27272a;\n  --accent-foreground: #fafafa;\n  --popover: #27272a;\n  --popover-foreground: #fafafa;\n  --border: #ffffff1a;\n  --input: #ffffff26;\n  --ring: #71717a;\n  --destructive: #f87171;\n}\n\n@layer base {\n  :where(.spreadsheet, .spreadsheet *) {\n    border-color: var(--border);\n  }\n}\n","path":"/spreadsheet.css","target":"uicapsule/spreadsheet/spreadsheet.css","type":"registry:file"},{"content":"declare module \"*.css\" {}\n","path":"/styles.d.ts","target":"uicapsule/spreadsheet/styles.d.ts","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/spreadsheet","name":"spreadsheet","registryDependencies":[],"title":"Spreadsheet","type":"registry:block"}