{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mini-sheet",
  "type": "registry:component",
  "title": "Mini Sheet",
  "description": "A pocket spreadsheet on coss ui table styles: drag or shift-select a range, type to edit, copy and paste straight from your sheets app, sort with rows that slide into order, and undo any of it.",
  "categories": [
    "component",
    "Numbers"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json",
    "https://coss.com/ui/r/kbd.json",
    "https://coss.com/ui/r/table.json"
  ],
  "files": [
    {
      "path": "registry/devl/mini-sheet.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon, CheckIcon, Redo2Icon, Undo2Icon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ClipboardEvent,\n  type KeyboardEvent,\n  type PointerEvent,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Kbd } from \"@/components/ui/kbd\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\";\nimport { cn } from \"@/lib/utils\";\n\nexport type CellValue = string | number | null;\n\nexport interface SheetColumn {\n  key: string;\n  label: string;\n  type?: \"text\" | \"number\";\n  format?: Intl.NumberFormatOptions;\n  width?: number;\n}\n\nexport interface SheetRow {\n  id: string;\n  [key: string]: CellValue;\n}\n\nexport interface MiniSheetProps {\n  columns?: SheetColumn[];\n  rows?: SheetRow[];\n  onRowsChange?: (rows: SheetRow[]) => void;\n  locale?: string;\n  className?: string;\n}\n\ntype Cell = { r: number; c: number };\ntype Sort = { key: string; dir: \"asc\" | \"desc\" } | null;\ntype Editing = { r: number; c: number; value: string; replace: boolean; session: number } | null;\n\nconst MotionRow = motion.create(TableRow);\n\nconst defaultColumns: SheetColumn[] = [\n  { key: \"company\", label: \"Company\", width: 152 },\n  { key: \"owner\", label: \"Owner\", width: 112 },\n  { key: \"plan\", label: \"Plan\", width: 92 },\n  { key: \"seats\", label: \"Seats\", type: \"number\", width: 72 },\n  { key: \"mrr\", label: \"MRR\", type: \"number\", width: 96, format: { style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 } },\n];\n\nconst defaultRows: SheetRow[] = [\n  { id: \"1\", company: \"Bluebird Labs\", owner: \"Maya Ortiz\", plan: \"Team\", seats: 24, mrr: 1920 },\n  { id: \"2\", company: \"Kestrel Freight\", owner: \"Jon Bakker\", plan: \"Enterprise\", seats: 140, mrr: 11200 },\n  { id: \"3\", company: \"Pinecone Studio\", owner: \"Ava Lindqvist\", plan: \"Pro\", seats: 6, mrr: 294 },\n  { id: \"4\", company: \"Harbor Analytics\", owner: \"Theo Nakamura\", plan: \"Team\", seats: 38, mrr: 3040 },\n  { id: \"5\", company: \"Orchard Health\", owner: \"Priya Raman\", plan: \"Enterprise\", seats: 92, mrr: 7360 },\n  { id: \"6\", company: \"Lumen & Co\", owner: \"Sam Okafor\", plan: \"Starter\", seats: 3, mrr: 57 },\n  { id: \"7\", company: \"Atlas Robotics\", owner: \"Lena Fischer\", plan: \"Pro\", seats: 12, mrr: 588 },\n];\n\nconst colName = (c: number) => String.fromCharCode(65 + c);\n\nfunction offsetWithin(el: HTMLElement, root: HTMLElement) {\n  let x = 0;\n  let y = 0;\n  let node: HTMLElement | null = el;\n  while (node && node !== root) {\n    x += node.offsetLeft;\n    y += node.offsetTop;\n    node = node.offsetParent as HTMLElement | null;\n  }\n  return { x, y };\n}\n\nfunction parseCell(raw: string, column: SheetColumn): CellValue {\n  const text = raw.trim();\n  if (column.type !== \"number\") return text;\n  if (text === \"\") return null;\n  const n = Number(text.replace(/[^0-9.\\-]/g, \"\"));\n  return Number.isFinite(n) ? n : Number.NaN;\n}\n\nexport function MiniSheet({\n  columns = defaultColumns,\n  rows: initialRows = defaultRows,\n  onRowsChange,\n  locale = \"en-US\",\n  className,\n}: MiniSheetProps) {\n  const reduce = useReducedMotion();\n  const uid = useId().replace(/:/g, \"\");\n  const wrapRef = useRef<HTMLDivElement>(null);\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const gridRef = useRef<HTMLTableElement>(null);\n  const dragging = useRef(false);\n  const session = useRef(0);\n  const finished = useRef(-1);\n\n  const [rows, setRows] = useState(initialRows);\n  const [past, setPast] = useState<SheetRow[][]>([]);\n  const [future, setFuture] = useState<SheetRow[][]>([]);\n  const [sort, setSort] = useState<Sort>(null);\n  const [anchor, setAnchor] = useState<Cell>({ r: 0, c: 0 });\n  const [focus, setFocus] = useState<Cell>({ r: 0, c: 0 });\n  const [editing, setEditing] = useState<Editing>(null);\n  const [hasFocus, setHasFocus] = useState(false);\n  const [notice, setNotice] = useState<{ id: number; text: string } | null>(null);\n  const [frame, setFrame] = useState<{ x: number; y: number; w: number; h: number } | null>(null);\n  const [glide, setGlide] = useState(true);\n\n  const formatters = useMemo(\n    () => columns.map((col) => (col.type === \"number\" ? new Intl.NumberFormat(locale, col.format) : null)),\n    [columns, locale],\n  );\n\n  const view = useMemo(() => {\n    if (!sort) return rows;\n    const col = columns.find((c) => c.key === sort.key);\n    const sign = sort.dir === \"asc\" ? 1 : -1;\n    return [...rows].sort((a, b) => {\n      const x = a[sort.key];\n      const y = b[sort.key];\n      if (x === null || x === \"\") return 1;\n      if (y === null || y === \"\") return -1;\n      if (col?.type === \"number\") return (Number(x) - Number(y)) * sign;\n      return String(x).localeCompare(String(y), locale) * sign;\n    });\n  }, [rows, sort, columns, locale]);\n\n  const maxR = view.length - 1;\n  const maxC = columns.length - 1;\n  const top = Math.min(anchor.r, focus.r);\n  const bottom = Math.max(anchor.r, focus.r);\n  const left = Math.min(anchor.c, focus.c);\n  const right = Math.max(anchor.c, focus.c);\n  const inRange = (r: number, c: number) => r >= top && r <= bottom && c >= left && c <= right;\n  const cellId = (r: number, c: number) => `${uid}-${r}-${c}`;\n\n  useEffect(() => {\n    if (!notice) return;\n    const t = setTimeout(() => setNotice(null), 1600);\n    return () => clearTimeout(t);\n  }, [notice]);\n\n  // Measured with offsets so row layout animations don't skew the frame.\n  useLayoutEffect(() => {\n    const wrap = wrapRef.current;\n    const grid = gridRef.current;\n    if (!wrap || !grid) return;\n    const a = grid.querySelector<HTMLElement>(`#${CSS.escape(cellId(top, left))}`);\n    const b = grid.querySelector<HTMLElement>(`#${CSS.escape(cellId(bottom, right))}`);\n    if (!a || !b) {\n      setFrame(null);\n      return;\n    }\n    const pa = offsetWithin(a, wrap);\n    const pb = offsetWithin(b, wrap);\n    setFrame({ x: pa.x, y: pa.y, w: pb.x + b.offsetWidth - pa.x, h: pb.y + b.offsetHeight - pa.y });\n  }, [top, left, bottom, right, view, columns]);\n\n  useEffect(() => {\n    const scroller = scrollRef.current;\n    const wrap = wrapRef.current;\n    const cell = gridRef.current?.querySelector<HTMLElement>(`#${CSS.escape(cellId(focus.r, focus.c))}`);\n    if (!hasFocus || !scroller || !wrap || !cell) return;\n    const x = offsetWithin(cell, wrap).x;\n    const rowHead = 36;\n    if (x - rowHead < scroller.scrollLeft) scroller.scrollLeft = x - rowHead;\n    else if (x + cell.offsetWidth > scroller.scrollLeft + scroller.clientWidth)\n      scroller.scrollLeft = x + cell.offsetWidth - scroller.clientWidth;\n  }, [focus, hasFocus]);\n\n  function display(value: CellValue, c: number) {\n    if (value === null || value === \"\") return \"\";\n    const f = formatters[c];\n    return f && typeof value === \"number\" ? f.format(value) : String(value);\n  }\n\n  function commitRows(next: SheetRow[]) {\n    setPast((p) => [...p.slice(-49), rows]);\n    setFuture([]);\n    setRows(next);\n    onRowsChange?.(next);\n  }\n\n  function writeCells(cells: { r: number; c: number; value: CellValue }[]) {\n    const byId = new Map(rows.map((row) => [row.id, { ...row }]));\n    let changed = false;\n    for (const { r, c, value } of cells) {\n      const viewRow = view[r];\n      const col = columns[c];\n      if (!viewRow || !col) continue;\n      if (typeof value === \"number\" && Number.isNaN(value)) continue;\n      const target = byId.get(viewRow.id);\n      if (target && target[col.key] !== value) {\n        target[col.key] = value;\n        changed = true;\n      }\n    }\n    if (changed) commitRows(rows.map((row) => byId.get(row.id) ?? row));\n    return changed;\n  }\n\n  function undo() {\n    const prev = past[past.length - 1];\n    if (!prev) return;\n    setPast((p) => p.slice(0, -1));\n    setFuture((f) => [rows, ...f]);\n    setRows(prev);\n    onRowsChange?.(prev);\n    setNotice({ id: Date.now(), text: \"Undone\" });\n  }\n\n  function redo() {\n    const next = future[0];\n    if (!next) return;\n    setFuture((f) => f.slice(1));\n    setPast((p) => [...p, rows]);\n    setRows(next);\n    onRowsChange?.(next);\n    setNotice({ id: Date.now(), text: \"Redone\" });\n  }\n\n  function select(cell: Cell, extend: boolean) {\n    const clamped = { r: Math.max(0, Math.min(maxR, cell.r)), c: Math.max(0, Math.min(maxC, cell.c)) };\n    if (extend) setFocus(clamped);\n    else {\n      setAnchor(clamped);\n      setFocus(clamped);\n    }\n  }\n\n  function startEdit(replaceWith?: string) {\n    const value = view[anchor.r]?.[columns[anchor.c].key] ?? null;\n    setFocus(anchor);\n    setEditing({\n      r: anchor.r,\n      c: anchor.c,\n      value: replaceWith ?? (value === null ? \"\" : String(value)),\n      replace: replaceWith !== undefined,\n      session: ++session.current,\n    });\n  }\n\n  function finishEdit(move: \"down\" | \"right\" | \"left\" | \"none\", cancel = false) {\n    if (!editing || finished.current === editing.session) return;\n    finished.current = editing.session;\n    if (gridRef.current?.contains(document.activeElement)) gridRef.current.focus();\n    if (!cancel) {\n      const value = parseCell(editing.value, columns[editing.c]);\n      writeCells([{ r: editing.r, c: editing.c, value }]);\n    }\n    setEditing(null);\n    if (move === \"down\") select({ r: editing.r + 1, c: editing.c }, false);\n    if (move === \"right\") select({ r: editing.r, c: editing.c + 1 }, false);\n    if (move === \"left\") select({ r: editing.r, c: editing.c - 1 }, false);\n  }\n\n  function cycleSort(key: string) {\n    setGlide(true);\n    setSort((s) => (!s || s.key !== key ? { key, dir: \"asc\" } : s.dir === \"asc\" ? { key, dir: \"desc\" } : null));\n  }\n\n  function rangeText() {\n    const lines: string[] = [];\n    for (let r = top; r <= bottom; r++) {\n      const cells: string[] = [];\n      for (let c = left; c <= right; c++) {\n        const v = view[r]?.[columns[c].key];\n        cells.push(v === null || v === undefined ? \"\" : String(v));\n      }\n      lines.push(cells.join(\"\\t\"));\n    }\n    return lines.join(\"\\n\");\n  }\n\n  const cellCount = (bottom - top + 1) * (right - left + 1);\n\n  function onCopy(e: ClipboardEvent<HTMLTableElement>) {\n    if (editing) return;\n    e.preventDefault();\n    e.clipboardData.setData(\"text/plain\", rangeText());\n    setNotice({ id: Date.now(), text: `Copied ${cellCount} ${cellCount === 1 ? \"cell\" : \"cells\"}` });\n  }\n\n  function clearRange() {\n    const cells = [];\n    for (let r = top; r <= bottom; r++)\n      for (let c = left; c <= right; c++) cells.push({ r, c, value: columns[c].type === \"number\" ? null : \"\" });\n    return writeCells(cells);\n  }\n\n  function onCut(e: ClipboardEvent<HTMLTableElement>) {\n    if (editing) return;\n    e.preventDefault();\n    e.clipboardData.setData(\"text/plain\", rangeText());\n    clearRange();\n    setNotice({ id: Date.now(), text: `Cut ${cellCount} ${cellCount === 1 ? \"cell\" : \"cells\"}` });\n  }\n\n  function onPaste(e: ClipboardEvent<HTMLTableElement>) {\n    if (editing) return;\n    e.preventDefault();\n    const text = e.clipboardData.getData(\"text/plain\").replace(/\\r/g, \"\").replace(/\\n$/, \"\");\n    if (!text) return;\n    const grid = text.split(\"\\n\").map((line) => line.split(\"\\t\"));\n    const cells: { r: number; c: number; value: CellValue }[] = [];\n    const single = grid.length === 1 && grid[0].length === 1;\n    if (single && cellCount > 1) {\n      for (let r = top; r <= bottom; r++)\n        for (let c = left; c <= right; c++) cells.push({ r, c, value: parseCell(grid[0][0], columns[c]) });\n    } else {\n      grid.forEach((line, dr) =>\n        line.forEach((raw, dc) => {\n          const r = top + dr;\n          const c = left + dc;\n          if (r <= maxR && c <= maxC) cells.push({ r, c, value: parseCell(raw, columns[c]) });\n        }),\n      );\n      const lastR = Math.min(maxR, top + grid.length - 1);\n      const lastC = Math.min(maxC, left + Math.max(...grid.map((l) => l.length)) - 1);\n      setAnchor({ r: top, c: left });\n      setFocus({ r: lastR, c: lastC });\n    }\n    if (writeCells(cells)) setNotice({ id: Date.now(), text: `Pasted ${cells.length} ${cells.length === 1 ? \"cell\" : \"cells\"}` });\n  }\n\n  function onKeyDown(e: KeyboardEvent<HTMLTableElement>) {\n    if (editing || view.length === 0) return;\n    setGlide(false);\n    const mod = e.metaKey || e.ctrlKey;\n    const key = e.key;\n    if (mod && key.toLowerCase() === \"z\") {\n      e.preventDefault();\n      if (e.shiftKey) redo();\n      else undo();\n      return;\n    }\n    if (mod && key.toLowerCase() === \"y\") {\n      e.preventDefault();\n      redo();\n      return;\n    }\n    if (mod && key.toLowerCase() === \"a\") {\n      e.preventDefault();\n      setAnchor({ r: 0, c: 0 });\n      setFocus({ r: maxR, c: maxC });\n      return;\n    }\n    const moves: Record<string, [number, number]> = {\n      ArrowUp: [-1, 0],\n      ArrowDown: [1, 0],\n      ArrowLeft: [0, -1],\n      ArrowRight: [0, 1],\n    };\n    if (moves[key]) {\n      e.preventDefault();\n      const [dr, dc] = moves[key];\n      const from = e.shiftKey ? focus : anchor;\n      const jump = mod ? { r: dr ? (dr > 0 ? maxR : 0) : from.r, c: dc ? (dc > 0 ? maxC : 0) : from.c } : null;\n      select(jump ?? { r: from.r + dr, c: from.c + dc }, e.shiftKey);\n      return;\n    }\n    if (key === \"Enter\" || key === \"F2\") {\n      e.preventDefault();\n      startEdit();\n      return;\n    }\n    if (key === \"Backspace\" || key === \"Delete\") {\n      e.preventDefault();\n      clearRange();\n      return;\n    }\n    if (key === \"Escape\") {\n      setFocus(anchor);\n      return;\n    }\n    if (key.length === 1 && !mod && !e.altKey) {\n      e.preventDefault();\n      startEdit(key);\n    }\n  }\n\n  function cellFromPoint(x: number, y: number): Cell | null {\n    const el = document.elementFromPoint(x, y)?.closest<HTMLElement>(\"[data-cell]\");\n    if (!el || !gridRef.current?.contains(el)) return null;\n    return { r: Number(el.dataset.r), c: Number(el.dataset.c) };\n  }\n\n  function onPointerDown(e: PointerEvent<HTMLTableElement>) {\n    if (e.button !== 0) return;\n    setGlide(true);\n    const target = e.target as HTMLElement;\n    if (target.closest(\"input, button\")) return;\n    const rowHead = target.closest<HTMLElement>(\"[data-row-head]\");\n    if (rowHead) {\n      const r = Number(rowHead.dataset.rowHead);\n      if (e.shiftKey) setFocus({ r, c: maxC });\n      else {\n        setAnchor({ r, c: 0 });\n        setFocus({ r, c: maxC });\n      }\n      e.preventDefault();\n      gridRef.current?.focus();\n      return;\n    }\n    const cell = cellFromPoint(e.clientX, e.clientY);\n    if (!cell) return;\n    e.preventDefault();\n    gridRef.current?.focus();\n    select(cell, e.shiftKey);\n    dragging.current = true;\n    e.currentTarget.setPointerCapture(e.pointerId);\n  }\n\n  function onPointerMove(e: PointerEvent<HTMLTableElement>) {\n    if (!dragging.current) return;\n    const cell = cellFromPoint(e.clientX, e.clientY);\n    if (cell && (cell.r !== focus.r || cell.c !== focus.c)) setFocus(cell);\n  }\n\n  function onPointerUp(e: PointerEvent<HTMLTableElement>) {\n    dragging.current = false;\n    if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);\n  }\n\n  let count = 0;\n  let sum = 0;\n  let numeric = 0;\n  const numericCols = new Set<number>();\n  for (let r = top; r <= bottom; r++) {\n    for (let c = left; c <= right; c++) {\n      const v = view[r]?.[columns[c].key];\n      if (v === null || v === \"\" || v === undefined) continue;\n      count++;\n      if (typeof v === \"number\") {\n        sum += v;\n        numeric++;\n        numericCols.add(c);\n      }\n    }\n  }\n  const statFmt =\n    numericCols.size === 1 ? formatters[[...numericCols][0]] : new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });\n  const ref =\n    cellCount === 1 ? `${colName(left)}${top + 1}` : `${colName(left)}${top + 1}:${colName(right)}${bottom + 1}`;\n  const frameMotion = reduce || !glide ? { duration: 0 } : { type: \"spring\" as const, stiffness: 700, damping: 50 };\n\n  return (\n    <div className={cn(\"w-full max-w-[640px] overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}>\n      <div className=\"flex items-center justify-between gap-2 border-b px-3 py-2\">\n        <div className=\"flex items-center gap-2\">\n          <span className=\"min-w-14 rounded-md bg-muted px-2 py-1 text-center font-mono text-xs tabular-nums\">{ref}</span>\n          <span className=\"truncate text-muted-foreground text-xs\">\n            {editing ? \"Editing · Enter to save, Esc to cancel\" : \"Double-click or type to edit\"}\n          </span>\n        </div>\n        <div className=\"flex items-center gap-1\">\n          <Button aria-label=\"Undo\" disabled={!past.length} onClick={undo} size=\"icon\" variant=\"ghost\">\n            <Undo2Icon />\n          </Button>\n          <Button aria-label=\"Redo\" disabled={!future.length} onClick={redo} size=\"icon\" variant=\"ghost\">\n            <Redo2Icon />\n          </Button>\n        </div>\n      </div>\n\n      <div className=\"overflow-x-auto\" ref={scrollRef}>\n        <div className=\"relative\" ref={wrapRef} style={{ minWidth: 36 + columns.reduce((sum, col) => sum + (col.width ?? 120), 0) }}>\n          <Table\n            aria-activedescendant={editing ? undefined : cellId(anchor.r, anchor.c)}\n            aria-colcount={columns.length}\n            aria-label=\"Spreadsheet\"\n            aria-multiselectable\n            aria-rowcount={view.length + 1}\n            className=\"table-fixed select-none outline-none\"\n            onBlur={(e) => {\n              if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setHasFocus(false);\n            }}\n            onCopy={onCopy}\n            onCut={onCut}\n            onDoubleClick={(e) => {\n              // Pointer capture retargets clicks to the table, so find the cell from the pointer position.\n              const cell = cellFromPoint(e.clientX, e.clientY);\n              if (!cell || editing) return;\n              const value = view[cell.r]?.[columns[cell.c].key] ?? null;\n              select(cell, false);\n              setEditing({\n                r: cell.r,\n                c: cell.c,\n                value: value === null ? \"\" : String(value),\n                replace: false,\n                session: ++session.current,\n              });\n            }}\n            onFocus={() => setHasFocus(true)}\n            onKeyDown={onKeyDown}\n            onPaste={onPaste}\n            onPointerCancel={onPointerUp}\n            onPointerDown={onPointerDown}\n            onPointerMove={onPointerMove}\n            onPointerUp={onPointerUp}\n            ref={gridRef}\n            role=\"grid\"\n            tabIndex={0}\n          >\n            <colgroup>\n              <col style={{ width: 36 }} />\n              {columns.map((col) => (\n                <col key={col.key} style={{ width: col.width }} />\n              ))}\n            </colgroup>\n            <TableHeader>\n              <TableRow className=\"hover:bg-transparent!\">\n                <TableHead className=\"h-9 px-0 text-center font-mono text-[10px]\" />\n                {columns.map((col, c) => {\n                  const dir = sort?.key === col.key ? sort.dir : null;\n                  const SortIcon = dir === \"asc\" ? ArrowUpIcon : dir === \"desc\" ? ArrowDownIcon : ArrowUpDownIcon;\n                  return (\n                    <TableHead\n                      aria-sort={dir === \"asc\" ? \"ascending\" : dir === \"desc\" ? \"descending\" : \"none\"}\n                      className={cn(\"h-9 px-1\", c >= left && c <= right && \"text-foreground\")}\n                      key={col.key}\n                    >\n                      <button\n                        className={cn(\n                          \"flex h-8 w-full cursor-pointer items-center gap-1 rounded-md px-1.5 text-xs outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring\",\n                          col.type === \"number\" && \"flex-row-reverse\",\n                        )}\n                        onClick={() => cycleSort(col.key)}\n                        type=\"button\"\n                      >\n                        <span className=\"truncate\">{col.label}</span>\n                        <motion.span\n                          animate={{ y: 0, opacity: dir ? 1 : 0.4 }}\n                          className=\"inline-flex\"\n                          key={dir ?? \"none\"}\n                          initial={{ y: dir === \"asc\" ? 4 : dir === \"desc\" ? -4 : 0, opacity: 0 }}\n                          transition={reduce ? { duration: 0 } : { type: \"spring\", duration: 0.2, bounce: 0 }}\n                        >\n                          <SortIcon className=\"size-3\" />\n                        </motion.span>\n                      </button>\n                    </TableHead>\n                  );\n                })}\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {view.length === 0 && (\n                <TableRow className=\"hover:bg-transparent!\">\n                  <TableCell className=\"h-24 text-center text-muted-foreground text-sm\" colSpan={columns.length + 1}>\n                    No rows yet. Paste from a spreadsheet to fill it.\n                  </TableCell>\n                </TableRow>\n              )}\n              {view.map((row, r) => (\n                <MotionRow\n                  aria-rowindex={r + 2}\n                  className=\"hover:bg-transparent!\"\n                  key={row.id}\n                  layout={reduce ? false : \"position\"}\n                  transition={{ type: \"spring\", stiffness: 380, damping: 34 }}\n                >\n                  <TableCell\n                    className={cn(\n                      \"cursor-pointer px-0 text-center font-mono text-[10px] text-muted-foreground tabular-nums transition-colors\",\n                      r >= top && r <= bottom && \"bg-muted text-foreground\",\n                    )}\n                    data-row-head={r}\n                  >\n                    {r + 1}\n                  </TableCell>\n                  {columns.map((col, c) => {\n                    const value = row[col.key] ?? null;\n                    const selected = inRange(r, c);\n                    const isEditing = editing?.r === r && editing.c === c;\n                    return (\n                      <TableCell\n                        aria-selected={selected}\n                        className={cn(\n                          \"relative h-9 cursor-cell truncate px-2.5 py-0 transition-colors\",\n                          col.type === \"number\" && \"text-right tabular-nums\",\n                          selected && cellCount > 1 && \"bg-info/8 dark:bg-info/12\",\n                        )}\n                        data-c={c}\n                        data-cell=\"\"\n                        data-r={r}\n                        id={cellId(r, c)}\n                        key={col.key}\n                        role=\"gridcell\"\n                      >\n                        {display(value, c)}\n                        {isEditing && editing && (\n                          <input\n                            aria-label={`Edit ${col.label}, row ${r + 1}`}\n                            autoFocus\n                            className={cn(\n                              \"absolute inset-0 z-20 w-full bg-popover px-2.5 text-sm outline-2 outline-info -outline-offset-2 rounded-[calc(var(--radius)*0.3)]\",\n                              col.type === \"number\" && \"text-right tabular-nums\",\n                            )}\n                            inputMode={col.type === \"number\" ? \"decimal\" : undefined}\n                            onBlur={() => finishEdit(\"none\")}\n                            onChange={(e) => setEditing({ ...editing, value: e.target.value })}\n                            onFocus={(e) => {\n                              const input = e.currentTarget;\n                              if (editing.replace) input.setSelectionRange(input.value.length, input.value.length);\n                              else input.select();\n                            }}\n                            onKeyDown={(e) => {\n                              e.stopPropagation();\n                              if (e.key === \"Enter\") {\n                                e.preventDefault();\n                                finishEdit(\"down\");\n                              } else if (e.key === \"Tab\") {\n                                e.preventDefault();\n                                finishEdit(e.shiftKey ? \"left\" : \"right\");\n                              } else if (e.key === \"Escape\") {\n                                e.preventDefault();\n                                finishEdit(\"none\", true);\n                              }\n                            }}\n                            value={editing.value}\n                          />\n                        )}\n                      </TableCell>\n                    );\n                  })}\n                </MotionRow>\n              ))}\n            </TableBody>\n          </Table>\n\n          {frame && (\n            <motion.div\n              animate={{ opacity: hasFocus || editing ? 1 : 0.45 }}\n              aria-hidden\n              className=\"pointer-events-none absolute inset-0 z-10\"\n              initial={false}\n              transition={{ duration: 0.15, ease: \"easeOut\" }}\n            >\n              <motion.span\n                animate={{ x: frame.x, y: frame.y, scaleX: frame.w }}\n                className=\"absolute top-0 left-0 h-0.5 w-px origin-left bg-info\"\n                initial={false}\n                transition={frameMotion}\n              />\n              <motion.span\n                animate={{ x: frame.x, y: frame.y + frame.h - 2, scaleX: frame.w }}\n                className=\"absolute top-0 left-0 h-0.5 w-px origin-left bg-info\"\n                initial={false}\n                transition={frameMotion}\n              />\n              <motion.span\n                animate={{ x: frame.x, y: frame.y, scaleY: frame.h }}\n                className=\"absolute top-0 left-0 h-px w-0.5 origin-top bg-info\"\n                initial={false}\n                transition={frameMotion}\n              />\n              <motion.span\n                animate={{ x: frame.x + frame.w - 2, y: frame.y, scaleY: frame.h }}\n                className=\"absolute top-0 left-0 h-px w-0.5 origin-top bg-info\"\n                initial={false}\n                transition={frameMotion}\n              />\n              <motion.span\n                animate={{ x: frame.x + frame.w - 4, y: frame.y + frame.h - 4 }}\n                className=\"absolute top-0 left-0 size-1.5 rounded-[1px] border border-card bg-info\"\n                initial={false}\n                transition={frameMotion}\n              />\n            </motion.div>\n          )}\n        </div>\n      </div>\n\n      <div className=\"relative flex h-10 items-center justify-between gap-3 border-t px-3 text-muted-foreground text-xs\">\n        <div className=\"flex items-center gap-3 tabular-nums\">\n          <span>\n            Count <span className=\"ms-0.5 font-medium text-foreground\">{count}</span>\n          </span>\n          {numeric > 0 && (\n            <>\n              <span>\n                Sum <span className=\"ms-0.5 font-medium text-foreground\">{statFmt?.format(sum) ?? String(sum)}</span>\n              </span>\n              <span className=\"hidden sm:inline\">\n                Avg{\" \"}\n                <span className=\"ms-0.5 font-medium text-foreground\">{statFmt?.format(sum / numeric) ?? \"\"}</span>\n              </span>\n            </>\n          )}\n        </div>\n        <div className=\"hidden items-center gap-1 sm:flex\">\n          <Kbd>⌘C</Kbd>\n          <Kbd>⌘V</Kbd>\n          <Kbd>⌘Z</Kbd>\n        </div>\n        <AnimatePresence>\n          {notice && (\n            <motion.div\n              animate={{ opacity: 1, y: 0, scale: 1 }}\n              className=\"absolute right-3 flex items-center gap-1.5 rounded-full bg-primary px-2.5 py-1 font-medium text-primary-foreground shadow-xs/5\"\n              exit={{ opacity: 0, y: 4, scale: 0.95 }}\n              initial={{ opacity: 0, y: 6, scale: 0.95 }}\n              key={notice.id}\n              role=\"status\"\n              style={{ originX: 1 }}\n              transition={reduce ? { duration: 0 } : { type: \"spring\", duration: 0.25, bounce: 0 }}\n            >\n              <CheckIcon className=\"size-3\" />\n              {notice.text}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}