# Introduction

Introducing Spreadsheet 2, the ultimate spreadsheet component for React that empowers you to render cells and editors declaratively while fully customizing the components with React's power.

## Canvas

One of the key benefits of Spreadsheet 2 is its high-performance rendering using canvas. This approach enables all UI interactions to be built on top of canvas using DOM nodes, making it incredibly customizable.

With Spreadsheet 2, developers have the freedom to add additional behaviour, such as Drag n Drop, Overlays, Charts, and Embeds, all while leveraging the power of React.

## Declarative

Spreadsheet 2 takes the declarative approach to rendering, which means it renders exactly what you pass as props. This enables developers to effortlessly pick and choose the components they need and compose a spreadsheet that aligns with their vision.

Plus, you can easily add custom components like buttons, tabs, or any other UI element to enhance the appearance of your spreadsheet.

{% code overflow="wrap" %}

```tsx
import "@rowsncolumns/spreadsheet/dist/spreadsheet.min.css";
import { 
  SpreadsheetProvider, 
  Spreadsheet, 
  FormulaBar,
  FormulaInput,
  CanvasGrid,
  Toolbar,
  BottomBar,
  NewSheetButton,
  SheetSwitcher,
  SheetTabs
} from "@rowsncolumns/spreadsheet"

const App = () => {
  return (
    <SpreadsheetProvider>
      <Toolbar>
        <ButtonUndo onClick={undo} disabled={!canUndo} />
        <ButtonRedo onClick={redo} disabled={!canRedo} />
      </Toolbar>
      <FormulaBar>
        <FormulaInput />
      </FormulaBar>
      <CanvasGrid
        rowCount={1000}
        columnCount={1000}
        getCellData={(sheetId, rowIndex, columnIndex) => {
            return {
                fv: 'Foobar'
            }
        })}
      />
      <BottomBar>
        <NewSheetButton />
        <SheetSwitcher />
        <SheetTabs />
      </BottomBar>
    <SpreadsheetProvider>
  )
}
```

{% endcode %}

## Performance

Performance is a top priority for us at Spreadsheet 2. With our advanced virtualization technique, the canvas only renders what is visible on the screen, resulting in quick rendering that targets a smooth 60 frames per second (FPS).

Additionally, our ScrollSnap feature makes scrolling buttery smooth, even for larger datasets, providing a seamless user experience.

## Accessibility

Accessibility is a crucial aspect of our development process. We offer support for keyboard shortcuts and navigation, and our platform is natively compatible with both light and dark modes.

We also provide the option for developers to customise themes, ensuring optimal accessibility for users with low/tunnel vision.

## Mobile/Touch devices

We understand the importance of mobile devices, which is why Spreadsheet 2 is compatible with most mobile browsers.

In the rare event that you encounter any issues with scrolling or selecting, please don't hesitate to contact us at <support@rowsncolumns.app>. We're always here to help!

{% content-ref url="/pages/S73Yamyxjv48abZLSOnb" %}
[Installation](/getting-started/installation)
{% endcontent-ref %}


# License

Trial, Professional and Enterprise licenses to suit your needs

For more information on licensing and pricing, please visit the [**Pricing page**](https://rowsncolumns.app/pricing)

<table data-view="cards"><thead><tr><th></th><th></th><th></th></tr></thead><tbody><tr><td><strong>Solo/Trial license</strong></td><td>If you are a solo developer working on a single application.</td><td>Not for commercial use</td></tr><tr><td><strong>Professional license</strong></td><td>If you are a developer looking for a full featured spreadsheet for commercial usage</td><td></td></tr><tr><td><strong>Enterprise license</strong></td><td>If you are deploying the Spreadsheet inside your organization. Commercial use with no limit on number of applications</td><td></td></tr></tbody></table>


# Demos

Play around with Spreadsheet 2

Spreadsheet Demo - <https://www.rowsncolumns.app/demo>


# Installation

Install Spreadsheet 2 using your preferred package manager.

Spreadsheet 2 is hosted in github.com, hence you have to create a personal access token to install the npm package.

### Generate personal access token

Visit <https://github.com/settings/tokens> to generate a personal access token

### Login to Github

Enter your personal access token when prompted for password

```
npm login --scope=@rowsncolumns --registry=https://npm.pkg.github.com
```

You can also generate a new personal access token and add this to your `.npmrc` file

{% code overflow="wrap" %}

```sh
//npm.pkg.github.com/:_authToken=PERSONAL_ACCESS_TOKEN_FROM_https://github.com/settings/tokens
@rowsncolumns:registry=https://npm.pkg.github.com/
```

{% endcode %}

***

## Install using Yarn or NPM

To install Spreadsheet 2 locally using a package manager, run one of these commands:

{% tabs %}
{% tab title="yarn" %}

```sh
yarn add @rowsncolumns/spreadsheet
```

```sh
// Optional spreadsheet state hook
yarn add @rowsncolumns/spreadsheet-state
```

{% endtab %}

{% tab title="npm" %}

```sh
npm install @rowsncolumns/spreadsheet --save
```

```bash
// Optional spreadsheet state hook
npm install @rowsncolumns/spreadsheet-state --save
```

{% endtab %}
{% endtabs %}

## Render the Spreadsheet

The code below will render a stateless spreadsheet in your React application.

```jsx
import React from 'react'
// Add css
import "@rowsncolumns/spreadsheet/dist/spreadsheet.min.css";
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet"

const MySpreadsheet = () => {
  const licenseKey = "will be emailed to you after purchase"
  return (
    <SpreadsheetProvider>
      <CanvasGrid licenseKey={licenseKey} />
    </SpreadsheetProvider>
  )
}
```

## Adding state

Spreadsheet 2 does not manage any state internally, It is a controlled component.

Developers can pass Spreadsheet state as props and hook into callbacks and continue updating this state externally.

{% content-ref url="/pages/Tkh9IZ8zoIKPkzF0SCal" %}
[Spreadsheet state](/getting-started/spreadsheet-state)
{% endcontent-ref %}

{% content-ref url="/pages/1K1LQHEUyu2ao77EyOsn" %}
[Headless UI](/getting-started/headless-ui)
{% endcontent-ref %}

## Adjusting the height of the Spreadsheet

The Spreadsheet automatically takes the height of the parent container.

```html
// Auto-height
<div style={{ flex: 1}}>
    <Spreadsheet />
</div>

// Fixed height
<div style={{ height: 500 }}>
    <Spreadsheet />
</div>
```

## Rending multiple spreadsheets

`SpreadsheetProvider` isolates Spreadsheet state to its own container. If you need multiple spreadsheets in a single page, wrap each spreadsheet with the provider

```tsx
import "@rowsncolumns/spreadsheet/dist/spreadsheet.min.css";
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet"

const SpreadsheetA = () => {
  return (
    <SpreadsheetProvider>
      <CanvasGrid />
    </SpreadsheetProvider>
  )
}
const SpreadsheetB = () => {
  return (
    <SpreadsheetProvider>
      <CanvasGrid />
    </SpreadsheetProvider>
  )
}

const App = () => {
  return (
    <>
      <SpreadsheetA />
      <SpreadsheetB />
    </>
  )
}
```

### Customising colors and themes

{% content-ref url="/pages/0G598eXeb4ZEewC1z2og" %}
[Theming](/configuration/features/theming)
{% endcontent-ref %}


# Spreadsheet state

Use hooks to manage spreadsheet state

Spreadsheet 2 is stateless and controlled React component. Developers can create their own data management model, using React state/Immer or any other state management library.

For quick start we recommend using `useSpreadsheetState` hook to manage the spreadsheet state. It has all built in features such as undo/redo, internal data models for Sheet, Sheet Data etc.

## useSpreadsheetState hook

`useSpreadsheetState` is a hook that can be installed as a separate package to render a completely stateful Spreadsheet 2 component.

{% tabs %}
{% tab title="yarn" %}

```sh
yarn add @rowsncolumns/spreadsheet-state
```

{% endtab %}

{% tab title="npm" %}

```sh
npm install @rowsncolumns/spreadsheet-state
```

{% endtab %}
{% endtabs %}

## Adding the hook

> Always use `SpreadsheetProvider` to wrap your component beforing using the hook.

SheetData is organized as an array of rows, where each row holds an array of cells. To find a specific cell within the sheet, you'd access it via its column and row indexes.

For instance, if you're looking for the cell at column 1 and row 1 (A1) in sheet 999, you'd find its CellData at sheetData\[999]\[1].values\[1].

SheetData is set up as a sparse array, meaning it only includes indexes that actually contain CellData. Empty indexes are either undefined/null or represented by empty objects.

Remember, the first row (index 0) and the first column (index 0) of SheetData are reserved for headers and should not be used to store data. So, please refrain from saving any information in sheetData\[999]\[0] or sheetData\[999]\[0].values\[0].

{% code overflow="wrap" %}

```tsx
import React, { useState } from "react"
import {
  useSpreadsheetState,
  SheeData,
  CellXfs
} from "@rowsncolumns/spreadsheet-state"
import { functionDescriptions, functions } from "@rowsncolumns/functions";
import {
  SpreadsheetProvider,
  CanvasGrid,
  CellData,
  Sheet,
  EmbeddedChart,
  EmbeddedObject,
  TableView,
  PivotTable
} from "@rowsncolumns/spreadsheet"

const MySpreadSheet = () => {
  const [sheets, onChangeSheets] = useState<Sheet[]>([]);
  const [sheetData, onChangeSheetData] = useState<SheetData<CellData>>({});
  const [cellXfs, onChangeCellXfs] = useState<CellXfs | null | undefined>(new Map());
  const [scale, onChangeScale] = useState(1);
  const [charts, onChangeCharts] = useState<EmbeddedChart[]>([]);
  const [embeds, onChangeEmbeds] = useState<EmbeddedObject[]>([]);
  const [tables, onChangeTables] = useState<TableView[]>([]);
  const [pivotTables, onChangePivotTables] = useState<PivotTable[]>([]);
  const [conditionalFormats, onChangeConditionalFormats] = useState<
    ConditionalFormatRule[]
  >([]);
  const [protectedRanges, onChangeProtectedRanges] = useState<ProtectedRange[]>(
    []
  );

  const {
    activeCell,
    activeSheetId,
    selections,
    rowCount,
    columnCount,
    frozenColumnCount,
    frozenRowCount,
    spreadsheetColors,
    spreadsheetTheme,
    rowMetadata,
    columnMetadata,
    merges,
    canRedo,
    redo,
    canUndo,
    undo,
    getCellData,
    getSheetName,
    getSheetId,
    getUserEnteredFormat,
    onRequestResultPreview,
    onChangeActiveCell,
    onChangeActiveSheet,
    onSelectNextSheet,
    onSelectPreviousSheet,
    onChangeSelections,
    onChange,
    onDelete,
    onChangeFormatting,
    onClearFormatting,
    onUnMergeCells,
    onMergeCells,
    onResize,
    onChangeBorder,
    onChangeDecimals,
    onChangeSheetTabColor,
    onRenameSheet,
    onDeleteSheet,
    onShowSheet,
    onHideSheet,
    onProtectSheet,
    onUnProtectSheet,
    onMoveSheet,
    onCreateNewSheet,
    onDuplicateSheet,
    onHideColumn,
    onShowColumn,
    onHideRow,
    onShowRow,
    onFill,
    onFillRange,
    onMoveChart,
    onResizeChart,
    onMoveEmbed,
    onResizeEmbed,
    onDeleteRow,
    onDeleteColumn,
    onDeleteCellsShiftUp,
    onDeleteCellsShiftLeft,
    onInsertCellsShiftRight,
    onInsertCellsShiftDown,
    onInsertRow,
    onInsertColumn,
    onMoveColumns,
    onMoveRows,
    onMoveSelection,
    onSortColumn,
    onSortRange,
    onFilterRange,
    onCopy,
    onPaste,
    cellXfsRegistry,
    getEffectiveFormat
  } = useSpreadsheetState({
     sheets,
     sheetData,
     tables,
     pivotTables,
     functions,
     namedRanges,
     theme,
     colorMode,
     conditionalFormats,
     cellXfs,
     onChangeSheets,
     onChangeSheetData,
     onChangeEmbeds,
     onChangeCharts,
     onChangeTables,
     onChangePivotTables,
     onChangeNamedRanges,
     onChangeTheme,
     onChangeCellXfs,
     onChangeHistory(patches) {
      onBroadcastPatch(patches);
     },
     onChangeProtectedRanges,
     onChangeConditionalFormats,
  })

  return (
    <CanvasGrid
      {...spreadsheetColors}
      stickyEditor={true}
      scale={scale}
      getEffectiveFormat={getEffectiveFormat}
      conditionalFormats={conditionalFormats}
      sheetId={activeSheetId}
      rowCount={rowCount}
      columnCount={columnCount}
      frozenColumnCount={frozenColumnCount}
      frozenRowCount={frozenRowCount}
      rowMetadata={rowMetadata}
      columnMetadata={columnMetadata}
      activeCell={activeCell}
      selections={selections}
      theme={spreadsheetTheme}
      merges={merges}
      charts={charts}
      embeds={embeds}
      tables={tables}
      protectedRanges={protectedRanges}
      onChangeActiveCell={onChangeActiveCell}
      onChangeSelections={onChangeSelections}
      onChangeActiveSheet={onChangeActiveSheet}
      onRequestResultPreview={onRequestResultPreview}
      onSelectNextSheet={onSelectNextSheet}
      onSelectPreviousSheet={onSelectPreviousSheet}
      onChangeFormatting={onChangeFormatting}
      onHideColumn={onHideColumn}
      onShowColumn={onShowColumn}
      onHideRow={onHideRow}
      onShowRow={onShowRow}
      onDelete={onDelete}
      onClearContents={onDelete}
      onFill={onFill}
      onFillRange={onFillRange}
      onResize={onResize}
      onMoveChart={onMoveChart}
      onMoveEmbed={onMoveEmbed}
      onResizeChart={onResizeChart}
      onResizeEmbed={onResizeEmbed}
      onDeleteRow={onDeleteRow}
      onDeleteColumn={onDeleteColumn}
      onDeleteCellsShiftUp={onDeleteCellsShiftUp}
      onDeleteCellsShiftLeft={onDeleteCellsShiftLeft}
      onInsertCellsShiftRight={onInsertCellsShiftRight}
      onInsertCellsShiftDown={onInsertCellsShiftDown}
      onInsertRow={onInsertRow}
      onInsertColumn={onInsertColumn}
      onMoveColumns={onMoveColumns}
      onMoveRows={onMoveRows}
      onMoveSelection={onMoveSelection}
      onCreateNewSheet={onCreateNewSheet}
      functionDescriptions={functionDescriptions}
      getSheetName={getSheetName}
      getSheetId={getSheetId}
      getCellData={getCellData}
      onChange={onChange}
      onUndo={undo}
      onRedo={redo}
      onSortColumn={onSortColumn}
      onSortRange={onSortRange}
      onFilterRange={onFilterRange}
      onClearFormatting={onClearFormatting}
      onCopy={onCopy}
      onPaste={onPaste}
    />
  )
      
}

const App = () => {
  return (
    <SpreadsheetProvider>
        <MySpreadSheet />
    </SpreadsheetProvider>
  )
}
```

{% endcode %}

#### Creating initial row data from primitives

To create initial sheet data you can use the helper function `createRowDataFromArray`

```tsx
import { createRowDataFromArray } from "@rowsncolumns/spreadsheet-state";

const [sheetData, onChangeSheetData] = useState<SheetData<CellData>>({
  1: createRowDataFromArray(
    [[], [null, "helo world", null, "=SUM(4,4)"]],
    1
  ),
});
```

## Using CellXfs for Shared Cell Formatting

`cellXfs` (Cell Extended Formatting Styles) is an optional feature that allows you to define reusable cell formats that can be shared across multiple cells. This is particularly useful for:

* Reducing memory footprint by sharing format definitions
* Maintaining consistent styling across cells
* Importing/exporting Excel files with shared styles

### Setting up cellXfs

`cellXfs` is a `Map` where keys are style IDs and values are format objects containing properties like `backgroundColor`, `textFormat`, `borders`, etc.

```tsx
const [cellXfs, onChangeCellXfs] = useState<CellXfs | null | undefined>(
  new Map([
    [
      "123",
      {
        backgroundColor: "green",
      },
    ],
  ])
);
```

### Using cellXfs in useSpreadsheetState

Pass `cellXfs` and `onChangeCellXfs` to the `useSpreadsheetState` hook:

```tsx
const {
  cellXfsRegistry,
  // ... other returned values
} = useSpreadsheetState({
  cellXfs,
  onChangeCellXfs,
  // ... other configuration
});
```

### Applying cellXfs to cells

To apply a cellXfs style to a cell, set the `sid` (style ID) property in the cell's `ef` (effective format) object:

```tsx
const cellData = {
  ef: { sid: "123" },  // References the cellXfs Map key
  fv: "Hello world",
  ev: { sv: "hello world" },
};
```

The `cellXfsRegistry` returned from the hook provides access to the shared styles and can be used for programmatic style management.

### Use `getEffectiveFormat` to pick up cellXfs format

```tsx
<CanvasGrid getEffectiveFormat={getEffectiveFormat} />
```


# Headless UI

Use any preferred state management library

At Spreadsheet, we understand the importance of flexibility, which is why our Headless UI approach allows you to use your preferred state management library with all components. Whether you prefer immer, redux, or any other state management library, you can seamlessly integrate it into your workflow.

For those who prefer the ease of use that comes with a pre-built solution, we offer the `@rowsncolumns/spreadsheet-state` package. This package uses immer to manage spreadsheet state, including sheets, sheetData, and more.

Additionally, it provides functionality for undo/redo, broadcasting history for collaboration, and easy integration with formula parser and evaluation libraries. For a full list of hooks that trigger side effects like formula calculation, history, and broadcasting, check out useSpreadsheetState.

Feel free to take a look at `useSpreadsheetState` for all hooks that triggers side effects (formula calculation, history and broadcasting)

## CellData

`CellData` is the per-cell record stored in the array/record that builds sheet data. It bundles everything we need to round-trip a cell: the user-entered value, the calculated value, formatting, hyperlinks, validation, comments, protection flags, pivot/expand state, and a handful of fields the spreadsheet engine uses internally.

Most fields exist in both a **long form** (`userEnteredValue`, `effectiveValue`, `userEnteredFormat`, `formattedValue`) and a **short form** (`ue`, `ev`, `uf`, `fv`). The short form is preferred for new writes — it keeps serialized payloads compact and is what the toolkit emits on save. The long forms still load, so older saved data keeps working.

A typical cell only sets one or two fields — most are optional and reserved for specific features.

```typescript
export type CellData<
  T extends StructuredResult = StructuredResult,
  M extends Mention = Mention,
> = {
  /**
   * The value the user entered. e.g. 1234, "Hello", or "=NOW()". Dates,
   * times, and datetimes are represented as doubles in serial format.
   */
  ue?: ExtendedValue;
  /**
   * The effective value. For formula cells, this is the calculated
   * result. For literal cells, equal to `ue`. Read-only — the
   * calculation engine writes this; do not set manually unless you're
   * hydrating from a saved snapshot.
   */
  ev?: ExtendedValue & StructuredValue<T>;
  /**
   * The formatted display string (after the number format is applied).
   * Read-only.
   */
  fv?: string;
  /**
   * The user-entered format. New writes are merged onto any existing
   * format. Can be a full `CellFormat` or a `StyleReference` (a short
   * `{ sid }` ref into the workbook's cellXfs registry) — the registry
   * dedupes repeated formats.
   */
  uf?: CellFormat | StyleReference;
  /**
   * Shared-strings key for cells whose value is interned in the
   * workbook's shared-strings table. Always a string.
   */
  ss?: string;
  /**
   * Hyperlink target. Either a plain URL string or a structured
   * value carrying the URL + display label + tooltip.
   */
  hyperlink?: string | HyperlinkValue;
  /**
   * Data-validation rule attached to the cell. Inline rule object or
   * an ID reference into the sheet-level validation registry.
   */
  dataValidation?: DataValidationRule | DataValidationRuleRecord["id"];
  /**
   * Plain-text note (Excel "comment" — single-author, not threaded).
   */
  note?: string;
  /**
   * Pointer into the threaded-comment store (Excel 2016+ replies and
   * resolved state).
   */
  commentThreadId?: string | number;
  /**
   * Citation reference (FILTER source attribution, etc.).
   */
  citationId?: Citation["id"];
  /**
   * Cell-level protection flags. Only take effect when sheet protection
   * is enabled. `locked` defaults to true in Excel, so this field is
   * usually set to `{ locked: false }` to opt a cell OUT of protection.
   */
  protection?: {
    locked?: boolean;
    hidden?: boolean;
  };
  /**
   * Image URL — for cells whose value is an embedded image.
   */
  imageUrl?: string;
  /**
   * Cell metadata marker — currently only "people" for mention chips.
   */
  metaType?: "people";
  /**
   * Array-formula spill range in A1 notation ("B5:B20"). Only set on
   * the anchor cell of an array formula; spilled cells in the range
   * carry no `af`. Round-trips to / from <f t="array" ref="..."/> in
   * xlsx.
   */
  af?: string;
  /**
   * Conditional-formatting results keyed by rule ID. Written by the
   * CF evaluator; consumers shouldn't set this directly.
   */
  conditionalFormattingResultById?: Record<string, CustomFormulaResult>;
  /**
   * Result of a formula-based data-validation rule. Same caveat as
   * `conditionalFormattingResultById`.
   */
  dataValidationResult?: CustomFormulaResult;
  /**
   * Outline / pivot grouping markers used by the canvas to render
   * expand-collapse chevrons.
   */
  expandable?: boolean;
  expanded?: boolean;
  /**
   * Pivot grouping key tuple for the cell.
   */
  groupKeys?: string[];
  /**
   * Number of children (for outline / pivot summary rows).
   */
  childrenCount?: number;
  /**
   * Pivot table this cell belongs to.
   */
  pivotId?: PivotTable["pivotId"];
  /**
   * Collaboration / sync metadata.
   */
  version?: number;
  updatedAt?: number;

  // -------------------------------------------------------------------
  // Long-form aliases — kept for backwards compatibility with older
  // saved payloads. Prefer the short forms (`ue` / `ev` / `uf` / `fv`)
  // for new writes.
  // -------------------------------------------------------------------
  /** @deprecated use `ue` */
  userEnteredValue?: ExtendedValue;
  /** @deprecated use `ev` */
  effectiveValue?: ExtendedValue & StructuredValue<T>;
  /** @deprecated use `fv` */
  formattedValue?: string;
  /** @deprecated use `uf` */
  userEnteredFormat?: CellFormat | StyleReference;
};
```

> **Effective format** is no longer persisted on `CellData`. The renderer derives it on the fly from `uf`, the cell's effective value, and (for formula cells) precedent formats — see `useSheetProperties.getEffectiveFormat`. The legacy `effectiveFormat` / `ef` fields are accepted for loading older data but should not be written.

## Using your own state

Here we are using React `useState` hook to manage Spreadsheet state.

> Do note that you will have to create your own undo/redo data structure for useState. Immer or MobX generates patches for each state update, which can then be undo'ed or redo'ed.

```tsx
import {
  SpreadsheetProvider,
  CanvasGrid,
  CellData,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import { CellInterface } from "@rowsncolumns/grid";
import { useState } from "react";

export type SheetData<T extends CellData = CellData> = Record<
  string,
  RowData<T>[]
>;
export type RowData<T> = {
  values?: T[];
};

const MySpreadsheet = () => {
  const activeSheet = 1;
  const [sheetData, setSheetData] = useState<SheetData>({
    1: [
      {},
      {
        values: [
          {},
          {
            fv: "Foobar",
          },
        ],
      },
    ],
  });
  const [sheets, setSheets] = useState<Sheet[]>([
    {
      sheetId: 1,
      rowCount: 10,
      columnCount: 10,
      title: "Sheet 1",
    },
  ]);
  return (
    <CanvasGrid
      rowCount={1000}
      columnCount={1000}
      sheetId={activeSheet}
      onChange={(
        sheetId: number,
        cell: CellInterface,
        value: string,
        previousValue: string
      ) => {
        // Save it back to state or database
      }}
      getCellData={(sheetId: number, rowIndex: number, columnIndex: number) => {
        return sheetData[sheetId]?.[rowIndex].values?.[columnIndex];
      }}
    />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```

## Custom CellData

You can inject a custom CellData type to Spreadsheet, and all renderers and editors will infer this type.

```tsx
type CustomCellData extends CellData {
  myNewProperty?: boolean
}

const MySpreadsheet = () => {
  return (
    <CanvasGrid<CustomCellData>
      rowCount={10}
      columnCount={10}
      getCellData={(sheetId, rowIndex, columnIndex) => {
        return {
          myNewProperty: false
        }
      }}
    />
  )
}
```


# Formula evaluation

Formula evaluation in Spreadsheet 2 with support for client-side calculation, Excel/Google Sheets compatibility, and custom functions.

Spreadsheet 2 provides a powerful formula evaluation engine with Excel/Google Sheets compatibility. Formula calculations can run on the main thread or in a Web Worker for better performance.

## Calculation Modes

The `useSpreadsheetState` hook supports two calculation modes:

### 1. Main Thread Calculation (Default)

Formulas are evaluated synchronously on the main UI thread. Best for smaller workbooks or when you need immediate results.

```typescript
import { useSpreadsheetState } from '@rowsncolumns/spreadsheet-state';

const MySpreadsheet = () => {
  const state = useSpreadsheetState({
    calculationMode: 'single', // Default
    initialSheets: [
      {
        id: 1,
        name: 'Sheet1',
        cells: [
          { rowIndex: 1, columnIndex: 1, fv: '5' },
          { rowIndex: 1, columnIndex: 2, ue: { fv: '=A1*2' } }
        ]
      }
    ]
  });

  return (
    <CanvasGrid
      {...state}
      rowCount={1000}
      columnCount={26}
    />
  );
};
```

**Pros:**

* Simpler setup (no Web Worker needed)
* Immediate results (no async overhead)
* Easier debugging

**Cons:**

* Can block UI for complex formulas
* Not suitable for workbooks with thousands of formulas

### 2. Web Worker Calculation

Formulas are evaluated asynchronously in a Web Worker thread. Best for larger workbooks or when you need non-blocking calculation.

```typescript
const createCalculationWorker = () => {
  if (typeof Worker === "undefined") {
    throw new Error(
      "The calculation worker requires a browser environment. " +
        "Provide a custom createCalculationWorker when rendering on the server."
    );
  }
  return new Worker(new URL("./calculation-worker.ts", import.meta.url), {
    type: "module",
  });
};
const MySpreadsheet = () => {
  const state = useSpreadsheetState({
    calculationMode: 'worker',
    createCalculationWorker,
    initialSheets: [/* ... */]
  });

  return <CanvasGrid {...state} />;
};
```

**Creating the Worker File:**

```javascript
// calculation-worker.ts
import { functions } from "@rowsncolumns/functions";
import { registerCalculationWorker } from '@rowsncolumns/calculation-worker/worker';

registerCalculationWorker({
  functions
});
```

**Pros:**

* Non-blocking UI during calculations
* Handles large workbooks smoothly
* Can leverage multiple CPU cores

**Cons:**

* Requires Web Worker setup
* Async results (slight delay)
* More complex debugging

## Excel/Google Sheets Compatibility

### Supported Features

We try to support full formula compatibility. If we have missed anything, let us know by email us at <support@rowsncolumns.app>

#### Formula Errors

All standard Excel error types are supported:

| Error     | Description                | Example                             |
| --------- | -------------------------- | ----------------------------------- |
| `#REF!`   | Invalid reference          | `=A1` (in cell A1)                  |
| `#NAME!`  | Unknown function           | `=UNKNOWNFUNC()`                    |
| `#VALUE!` | Wrong value type           | `=1 + "text"`                       |
| `#DIV/0!` | Division by zero           | `=1/0`                              |
| `#N/A`    | Value not available        | `=VLOOKUP("missing", A:B, 2)`       |
| `#NUM!`   | Invalid numeric value      | `=SQRT(-1)`                         |
| `#SPILL!` | Array blocked by data      | `=SEQUENCE(10)` when cells occupied |
| `#NULL!`  | Invalid range intersection | `=A1:A10 B1:B10`                    |

#### Array Formulas

Dynamic array formulas that spill across multiple cells (Excel 365 style):

```typescript
// Entering "=SEQUENCE(5)" in A1 automatically fills A1:A5
// A1: 1
// A2: 2
// A3: 3
// A4: 4
// A5: 5

// Works with 2D arrays
// =SEQUENCE(3, 2) creates a 3-row, 2-column array
```

**Spill Collision Handling:**

* If spill range overlaps user data, formula returns `#SPILL!` error
* Editing a spill cell clears the array and allows user input
* Deleting a spill cell restores the array formula

#### Circular References

Enable iterative calculation to resolve circular references (like Excel):

```typescript
const state = useSpreadsheetState({
  iterativeCalculation: {
    enabled: true,
    maxIterations: 100,    // Excel default: 100
    maxChange: 0.001       // Excel default: 0.001
  }
});

// Example: A1 = A1 + 1
// Without iterative: Shows #REF! error
// With iterative: Converges to stable value
```

**How it works:**

1. Formulas are evaluated repeatedly until convergence
2. Stops when change between iterations < `maxChange`
3. Returns `#NUM!` if doesn't converge after `maxIterations`

#### Named Ranges

Define reusable range names (Excel-compatible):

```typescript
const state = useSpreadsheetState({
  namedRanges: [
    {
      namedRangeId: 'sales-total',
      name: 'SalesTotal',
      range: {
        sheetId: 1,
        startRowIndex: 1,
        endRowIndex: 100,
        startColumnIndex: 1,
        endColumnIndex: 1
      }
    }
  ]
});

// Use in formula: =SUM(SalesTotal)
```

**Features:**

* Case-insensitive (SalesTotal = salestotal)
* Four shapes supported: **range-typed** (`MyRange = A1:B10`), **static value** (`MyVal = 42`), **formula-typed** (`MyVar = =SUM(A1:A10)` — recomputes when precedents change), and **named LAMBDAs** (`MyFunc = =LAMBDA(x, x*2)` — callable as `=MyFunc(5)`)
* Workbook + sheet scope (sheet-scoped names shadow workbook-scoped ones with the same display name)
* Order-independent registration — chained names (`fullPrice = price * 1.08`) wire DAG edges correctly even when listed before their dependencies
* Cross-sheet support
* DAG JSON round-trip via `Dag.toJSON` / `Dag.fromJSON`

See [Named ranges](/configuration/features/named-ranges) for the full reference.

#### Structured References (Excel Tables)

Excel-style table references with column names:

```typescript
const state = useSpreadsheetState({
  tables: [
    {
      id: 'inventory',
      title: 'Inventory',
      sheetId: 1,
      range: {
        sheetId: 1,
        startRowIndex: 1,
        endRowIndex: 10,
        startColumnIndex: 1,
        endColumnIndex: 3
      },
      headerRow: true,
      columns: [
        { name: 'Product' },
        { name: 'Quantity' },
        { name: 'Price' }
      ]
    }
  ]
});

// Formula examples:
// =Inventory[Quantity]           → Qty column (B2:B10)
// =Inventory[[#Headers],[Price]] → Price header (C1)
// =Inventory[@Quantity]          → Current row Qty
// =SUM(Inventory[Price])         → Sum of Price column
```

**Supported Specifiers:**

* `#Headers` - Header row
* `#Data` - Data rows only
* `#All` - Entire table including headers
* `#Totals` - Total row
* `@ThisRow` - Current row (implicit in `[@ColumnName]`)

#### Spilled-range operator `A1#`

Reference the current spill bounds of a dynamic array. Resolves via `Calculator.arrayRangeMap` to the live spilled range, so the reference auto-resizes when the source formula's output shape changes.

```
A1: =SEQUENCE(5)          // spills to A1:A5
B1: =SUM(A1#)             // sums the current spill range → 15
B2: =COUNT(A1#)           // counts the current spill range → 5
```

When `A1`'s formula changes shape (`=SEQUENCE(10)`), `A1#` re-resolves to `A1:A10` automatically.

#### LAMBDA family and higher-order functions

LAMBDA / LET / MAP / REDUCE / BYROW / BYCOL / SCAN / MAKEARRAY all ship, along with GROUPBY and PIVOTBY built on the same infrastructure. See [LAMBDA and higher-order functions](/configuration/features/lambda-and-higher-order-functions) for the full reference.

#### FORECAST.ETS family

Holt-Winters triple-exponential smoothing for time-series forecasting. Four functions ship:

* `FORECAST.ETS(target, values, timeline, [seasonality], [data_completion], [aggregation])` — predict a value at a future date.
* `FORECAST.ETS.CONFINT(target, values, timeline, [confidence], [seasonality])` — confidence interval (default 95%).
* `FORECAST.ETS.SEASONALITY(values, timeline)` — auto-detect the cycle length.
* `FORECAST.ETS.STAT(values, timeline, stat_type, [seasonality])` — diagnostic statistics (alpha, beta, gamma, MASE, SMAPE, MAE, RMSE, step size — `stat_type` 1..8).

Seasonality is auto-detected by comparing in-sample SSE across candidate periods. Pass `1` for `seasonality` to force a non-seasonal (Holt linear) fit.

```
=FORECAST.ETS(45000, A1:A100, B1:B100)
=FORECAST.ETS.SEASONALITY(A1:A100, B1:B100)
=FORECAST.ETS.CONFINT(45000, A1:A100, B1:B100, 0.95)
```

#### GETPIVOTDATA

Look up a value cell in a rendered pivot. Header-matched: pass alternating `(field, item)` tuples and the function scans the pivot's row + column header bands.

```
=GETPIVOTDATA("Revenue", $H$1, "Region", "North", "Quarter", "Q3")
```

The pivot anchor (`$H$1`) is the top-left corner cell of the rendered pivot.

### Supported Functions

The calculation engine includes 300+ Excel-compatible functions:

**Math & Trig:**

* `SUM`, `AVERAGE`, `COUNT`, `MIN`, `MAX`
* `ROUND`, `FLOOR`, `CEILING`, `ABS`, `SQRT`
* `SIN`, `COS`, `TAN`, `PI`, `POWER`

**Logical:**

* `IF`, `AND`, `OR`, `NOT`
* `IFERROR`, `IFNA`, `IFS`
* `SWITCH`, `TRUE`, `FALSE`

**Text:**

* `CONCAT`, `CONCATENATE`, `TEXTJOIN`
* `LEFT`, `RIGHT`, `MID`, `LEN`
* `UPPER`, `LOWER`, `PROPER`, `TRIM`
* `FIND`, `SEARCH`, `SUBSTITUTE`, `REPLACE`

**Lookup & Reference:**

* `VLOOKUP`, `HLOOKUP`, `XLOOKUP`
* `INDEX`, `MATCH`
* `INDIRECT`, `OFFSET`, `ROW`, `COLUMN`
* `FILTER`, `SORT`, `UNIQUE`

**Date & Time:**

* `NOW`, `TODAY`, `DATE`, `TIME`
* `YEAR`, `MONTH`, `DAY`, `HOUR`, `MINUTE`, `SECOND`
* `DATEDIF`, `NETWORKDAYS`, `WORKDAY`

**Array Functions:**

* `SEQUENCE`, `RANDARRAY`, `UNIQUE`
* `SORT`, `FILTER`, `TRANSPOSE`
* `MAKEARRAY`, `BYCOL`, `BYROW`

**Financial:**

* `PMT`, `PV`, `FV`, `RATE`, `NPER`
* `IPMT`, `PPMT`, `NPV`, `IRR`

**Statistical:**

* `STDEV`, `VAR`, `MEDIAN`, `MODE`
* `PERCENTILE`, `QUARTILE`
* `CORREL`, `COVARIANCE`

[Full function list →](https://github.com/rowsncolumns/spreadsheet/blob/main/docs/getting-started/functions/README.md)

## Custom Functions

Add your own functions to extend the calculation engine:

### Defining Custom Functions

```typescript
import FormulaParser from '@rowsncolumns/fast-formula-parser';

const customFunctions = {
  // Simple function
  DOUBLE: (parser: FormulaParser, value: number) => {
    return value * 2;
  },

  // Function with multiple arguments
  GREET: (parser: FormulaParser, firstName: string, lastName: string) => {
    return `Hello, ${firstName} ${lastName}!`;
  },

  // Context-aware function (accesses cell position)
  CURRENT_ROW: (parser: FormulaParser) => {
    return parser.position.row;
  },

  // Async function (for API calls, etc.)
  FETCH_PRICE: async (parser: FormulaParser, symbol: string) => {
    const response = await fetch(`/api/stock/${symbol}`);
    const data = await response.json();
    return data.price;
  },

  // Function with cancellation support
  SLOW_API: async (parser: FormulaParser, url: string) => {
    const response = await fetch(url, {
      signal: parser.position.signal // Cancels on recalc
    });
    return await response.text();
  }
};
```

### Registering Custom Functions

```typescript
const state = useSpreadsheetState({
  calculationMode: 'sync',
  functions: customFunctions
});
```

Or for Web Worker mode:

```javascript
// calculation-worker.js
import { registerCalculationWorker } from '@rowsncolumns/calculation-worker/worker';
import { myCustomFunctions } from './custom-functions';

registerCalculationWorker({
  functions: myCustomFunctions
});
```

### Function Autocomplete

Add descriptions for autocomplete suggestions:

```typescript
const functionDescriptions = [
  {
    datatype: "Number",
    title: "DOUBLE",
    syntax: "DOUBLE(value)",
    description: "Returns double the input value.",
    example: "DOUBLE(5)",
    usage: ["DOUBLE(A1)", "DOUBLE(10)"],
    parameters: [
      {
        title: "value",
        description: "The number to double."
      }
    ]
  }
];

// Pass to CanvasGrid
<CanvasGrid
  functionDescriptions={functionDescriptions}
  {...state}
/>
```

## Performance Optimization

### Tips for Large Workbooks

1. **Use Web Worker mode** for workbooks with >1000 formulas
2. **Avoid volatile functions** (`NOW()`, `RAND()`) in large ranges
3. **Batch updates** when modifying multiple cells:

```typescript
const { enqueueCalculation } = useSpreadSheetState(...)

for (let i = 1; i <= 100; i++) {
  enqueueCalculation({ position: { rowIndex: i, columnIndex: 1, sheetId: 1 }, type: 'add' });
}

```

4. **Disable auto-recalc** during bulk operations:

```typescript
const state = useSpreadsheetState({
  recalculateOnOpen: false,
});

// Import 1000 rows without recalculating
state.onChangeBatch(...);

// Trigger single recalculation
state.calculateNow();
```

5. **Use range formulas** instead of copying:

```typescript
// Bad: 1000 cells with =A1*2, =A2*2, =A3*2...
// Good: One array formula =A1:A1000*2
```

### Benchmarks

Typical performance on modern hardware:

| Operation                            | Dataset       | Time     |
| ------------------------------------ | ------------- | -------- |
| Simple formula (`=A1*2`)             | 1 cell        | <1ms     |
| Range formula (`=SUM(A1:A100)`)      | 100 cells     | 2-5ms    |
| Complex dependency chain (10 levels) | 1000 formulas | 50-100ms |
| Array formula (`=SEQUENCE(1000)`)    | 1000 cells    | 10-20ms  |
| Large workbook recalc                | 10K formulas  | 500ms-2s |

## Troubleshooting

### Formulas Not Updating

**Problem:** Changing a cell doesn't recalculate dependents

**Solutions:**

1. Check that formulas start with `=`
2. Verify `enqueueCalculation` is wired correctly (if custom)

### #REF! Errors

**Problem:** Valid formula shows #REF! error

**Solutions:**

1. Check sheet names match in `sheets` prop
2. Verify named ranges are registered
3. Enable iterative calculation for circular references:

   ```typescript
   iterativeCalculation: { enabled: true }
   ```

### Slow Performance

**Problem:** UI freezes during formula entry

**Solutions:**

1. Switch to Web Worker mode:

   ```typescript
   calculationMode: 'worker'
   ```
2. Avoid volatile functions in large ranges
3. Use manual calculation mode for bulk imports

### Custom Functions Not Working

**Problem:** Custom function returns #NAME! error

**Solutions:**

1. Verify function is registered in `functions` prop
2. For Web Worker mode, ensure function is in worker file
3. Check function name matches formula (case-insensitive)

## Package Information

The formula evaluation system is provided by:

* **Package:** `@rowsncolumns/calculation-worker`
* **Version:** 1.0.15+
* **License:** MIT

### Installation

```bash
npm install @rowsncolumns/calculation-worker
# or
yarn add @rowsncolumns/calculation-worker
```

## Further Reading

* [Custom Functions Guide](https://github.com/rowsncolumns/spreadsheet/blob/main/docs/getting-started/functions/named-functions.md)
* [Array Formulas Guide](https://github.com/rowsncolumns/spreadsheet/blob/main/docs/getting-started/functions/array-formulas.md)
* [Named Ranges Setup](https://github.com/rowsncolumns/spreadsheet/blob/main/docs/getting-started/configuration/features/named-ranges.md)
* [Structured References](https://github.com/rowsncolumns/spreadsheet/blob/main/docs/getting-started/configuration/features/structured-references/README.md)
* [Performance Optimization](https://github.com/rowsncolumns/spreadsheet/blob/main/docs/getting-started/configuration/features/calculate-on-demand.md)

## Support

For formula evaluation issues:

* GitHub: [rowsncolumns/spreadsheet/issues](https://github.com/rowsncolumns/spreadsheet/issues)
* Email: <support@rowsncolumns.app>
* Documentation: [docs.rowsncolumns.app](https://docs.rowsncolumns.app)

***

**Last Updated:** 2025-01-22


# Imperative Spreadsheet API

As an escape hatch, we bundle an imperative API for the Spreadsheet to control spreadsheet states

Imperative API is helpful when you want to trigger state changes without having to modify spreadsheet state manually. These APIs can do multiple actions in one function call

## How to use

1. Wrap your Spreadsheet in `SpreadsheetProvider`
2. Use the `useSpreadsheetApi` hook to access the imperative API
3. Make sure you have handlers for callbacks such as `onChange`

{% code overflow="wrap" %}

```typescript
import React, { useState } from 'react'
// Add css
import "@rowsncolumns/spreadsheet/dist/spreadsheet.min.css";
import { SpreadsheetProvider, CanvasGrid, useSpreadsheetApi } from "@rowsncolumns/spreadsheet"
import { SheetData, CellData } from "@rowsncolumns/spreadsheet-state"

const MySpreadsheet = () => {
  const api = useSpreadsheetApi()
  const [sheetData, setSheetData] =
      useState<SheetData<CellData>>({});
      
  const onChange = (sheetId, cell, value) => {
    setSheetData(prev => {
      // Update your sheet data
    })
  }
  
  return (
    <>
      <button
        onClick={() => {
          // Get the effective value of a cell
          const value = api?.getActiveSheet()
            ?.getRange({ rowIndex: 1, columnIndex: 1 })
            .getEffectiveValue()
          console.log('Cell value:', value)
          
          // Or chain multiple operations
          api?.getActiveSheet()
              ?.getRange({ rowIndex: 2, columnIndex: 2 })
              .setValue("hello world")
              .setFormat("backgroundColor", "#80FF08")
        }}
      >Update a cell</button>
      <CanvasGrid onChange={onChange} />
    </>
  )
}

const App = () => {
  return (
    <SpreadsheetProvider>
      <MySpreadsheet />
    </SpreadsheetProvider>
  )
}
```

{% endcode %}

## Batch updating Spreadsheet values

For batch update of values, we recommend using `setValues` API, so only 1 undo/redo history will be added

{% code overflow="wrap" %}

```typescript
const api = useSpreadsheetApi()

api?.getActiveSheet()?.getRange({
  startRowIndex: 1,
  endRowIndex: 2,
  startColumnIndex: 2,
  endColumnIndex: 3,
}).setValues([
  ['foo', 'bar'],
  ['hello', 'world']
])
```

{% endcode %}

## Export range to clipboard

You can export a range to the clipboard:

{% code overflow="wrap" %}

```typescript
const api = useSpreadsheetApi()

api?.exportRange?.(
  {
    startRowIndex: 1,
    endRowIndex: 10,
    startColumnIndex: 1,
    endColumnIndex: 5,
    sheetId: activeSheetId,
  },
  undefined,
  "clipboard"
)
```

{% endcode %}

## Spreadsheet

`getSheet(sheetId: number)`

`setActiveSheet(sheetId: number)`

`getActiveSheet()`

`insertSheet()`

## Sheet

`setActiveCell(cell: CellInterface)`

`setSelections(selections: GridRange)`

`setRowHeight(rowIndex: number, dimension: number)`

`setColumnWidth(columnIndex: number, dimension: number)`

`getActiveCell()`

`getSelections()`

`getSheetId()`

`getRange(range: GridRange | CellInterface)`

`hideRow(rowIndexes: number[])`

`hideColumn(columnIndexes: number[])`

`showRow(rowIndexes: number[])`

`showColumn(columnIndexes: number[])`

`deleteRow(rowIndexes: number[])`

`deleteColumn(columnIndexes: number[])`

`insertRow(rowIndex: number, numRows = 1)`

`insertColumn(columnIndex: number, numColumns = 1)`

`moveRows(rowIndexes: number[], destinationRow: number)`

`moveColumns(columnIndexes: number[], destinationColumn: number)`

`onRequestSearch()`

`sortRange( selections: SelectionArea[], sortOrder: SortOrder )`

`sortColumn(columnIndex: number, sortOrder: SortOrder)`

## CellRange

`setValue(text: string)` - Set value of a single cell

`setValues(values: string[][])` - Set values for a range of cells

`getEffectiveValue()` - Get the computed/formatted value of a cell

`clearFormatting()` - Clear all formatting from the range

`setFormat(type: T, value: FormattingValue)` - Set a specific format property (e.g., backgroundColor, numberFormat, indent)

`delete()` - Delete the content and formatting of the range

## Additional API Methods

`exportRange(range: SheetRange, format?: string, destination?: "clipboard")` - Export a range to clipboard or other destinations

`dispatchEvent(event: Event)` - Dispatch DOM events to the spreadsheet grid

`commit(): boolean` - Commit the in-progress cell edit. Submits when the value is dirty, cancels when clean, no-ops when no edit is active. Does not refocus the grid — focus stays where the user moved it. Returns `true` if there was an edit to flush.

```tsx
const api = useSpreadsheetApi()

<CanvasGrid onBlur={() => api?.commit()} />
```

The same method is available on the `ref` and via `useSpreadsheet()`:

```tsx
const ref = useRef<CanvasGridMethods>(null)
ref.current?.commit?.()

const { commit } = useSpreadsheet()
commit?.()
```


# Examples

Some examples to get your started with Spreadsheet 2

## Vite

{% embed url="<https://github.com/rowsncolumns/spreadsheet/tree/main/examples>" %}
Vite example
{% endembed %}

## NextJS

{% embed url="<https://github.com/rowsncolumns/spreadsheet/tree/main/examples/nextjs>" %}
NextJS example
{% endembed %}


# Excel compatibility

Feature compatibility comparison between Excel/Google Sheets and Rows n Columns Spreadsheet

A comprehensive feature-by-feature comparison between Excel/Google Sheets and the Rows n Columns Spreadsheet component.

Legend:

* ✅ Full support
* ⚠️ Partial / known gaps documented inline
* ❌ Not supported (deferred or out of scope)
* N/A Not applicable

## At a glance

| Capability                    | XLSX | XLSM | XLSB | XLS | ODS | CSV |
| ----------------------------- | ---- | ---- | ---- | --- | --- | --- |
| Import                        | ✅    | ✅    | ⚠️   | ⚠️  | ✅   | ✅   |
| Export                        | ✅    | ❌    | ❌    | ❌   | ✅   | ✅   |
| Password-encrypted file (CFB) | ⚠️   | ❌    | ❌    | ❌   | ❌   | N/A |

* **XLSM** imports identically to XLSX but discards macros/VBA on round-trip (security).
* **XLSB** / **XLS** parsers exist for read-only viewing; export round-trips through XLSX.
* **CSV** is value-only — no formatting, formulas, or sheet structure beyond rows/columns.
* Password-protected **XLSX export** uses CFB encryption (`libs/toolkit/exporter/xlsx/xlsx-export.ts`). Decryption of password-protected XLSX **import** is not implemented.

## Feature compatibility matrix

### Basic editing

| Feature                    | Excel      | Sheets | Rows n Columns | Notes                                                                                                                                                      |
| -------------------------- | ---------- | ------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cell editing               | ✅          | ✅      | ✅              |                                                                                                                                                            |
| Multi-cell selection       | ✅          | ✅      | ✅              | Including non-contiguous via Ctrl/Cmd+click                                                                                                                |
| Copy / paste / cut         | ✅          | ✅      | ✅              | Clipboard API + internal copy buffer (preserves formulas, styles, merges)                                                                                  |
| Paste special              | ✅          | ✅      | ✅              | Values only, formats only, formulas only, transpose                                                                                                        |
| Undo / redo                | ✅          | ✅      | ✅              | Immer patch-based; works alongside YJS / ShareDB                                                                                                           |
| Find & replace             | ✅          | ✅      | ✅              | Find pane + Cmd/Ctrl+H opens Replace; Replace + Replace All wired through `useOnFindReplace` (regex search, single-cell or whole-sheet scope, atomic undo) |
| Fill handle (drag to fill) | ✅          | ✅      | ✅              | Includes series detection (linear, date, weekday) + user-defined custom autofill lists via `useSpreadsheetState({customAutofillLists})`                    |
| Magic fill / smart fill    | ✅ (Sheets) | ✅      | ⚠️             | AI-driven via OpenAI integration; opt-in                                                                                                                   |
| Autofill from selection    | ✅          | ✅      | ✅              | `useOnFill`, `useOnFillRange`                                                                                                                              |
| Remove duplicates          | ✅          | ✅      | ✅              | `onRemoveDuplicates` callback                                                                                                                              |
| Text to columns            | ✅          | ✅      | ✅              | Delimited + fixed-width                                                                                                                                    |
| Drag & drop cells          | ✅          | ✅      | ✅              | Within sheet + drop external files                                                                                                                         |

### Cell formatting

| Feature                                                                                    | Excel | Sheets | Rows n Columns | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------------------------------------------------------------------------------ | ----- | ------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Font family                                                                                | ✅     | ✅      | ✅              | Resolved against workbook theme fonts                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Font size                                                                                  | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Bold / italic / underline / strikethrough                                                  | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Text color                                                                                 | ✅     | ✅      | ✅              | Theme + tint, indexed, RGB                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| Background color (solid)                                                                   | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Gradient cell fills                                                                        | ✅     | ❌      | ✅              | Linear (`degree`) + path (`left`/`right`/`top`/`bottom`) gradients with 2+ stops. Parser reads `<gradientFill>` from `xl/styles.xml`; exporter emits it back with `<stop position><color rgb=…/></stop>` children; canvas renderer paints via `createLinearGradient` / `createRadialGradient`. Gradient takes precedence over pattern fill (Excel UI treats them as mutually exclusive on one cell)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Number formats (built-in)                                                                  | ✅     | ✅      | ✅              | \~68 built-in formats                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Custom number format patterns                                                              | ✅     | ✅      | ✅              | Full pattern grammar — fractions (`# ?/?`), asterisk-padding (`*` ), locale tokens (`[$$-409]`, `[$€-2]`, `[$£-809]`, `[$¥-411]`), conditional color/comparison segments (`[Red][>100]…;[Blue][<0]…;…`), elapsed-time brackets (`[h]:mm`), millisecond precision (`hh:mm:ss.000`), 4-segment patterns with `@` string placeholder. Delegated to the `numfmt` library; audit tests in `libs/utils/__tests__/formatter-edge-cases.spec.ts`                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Decimal places (increase/decrease)                                                         | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Negative elapsed times (`[h]:mm` durations)                                                | ⚠️    | ✅      | ✅              | Rows n Columns displays negative durations with a minus sign and the correct magnitude (`-0:53:00`), matching Google Sheets and Excel's 1904 date system. Excel in the default 1900 date system renders any negative time value as `#######`, so an exported file shows `####` there until the workbook uses the 1904 system                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| Horizontal alignment                                                                       | ✅     | ✅      | ✅              | left, center, right, justify, distributed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Vertical alignment                                                                         | ✅     | ✅      | ✅              | top, middle, bottom                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Text wrap                                                                                  | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Text rotation (angle, vertical)                                                            | ✅     | ✅      | ✅              | 0–360° + vertical orientation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Shrink to fit                                                                              | ✅     | ❌      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Indent                                                                                     | ✅     | ✅      | ✅              | Increase / decrease                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Borders                                                                                    | ✅     | ✅      | ✅              | All 13 OOXML styles round-trip: solid (`thin`/`medium`/`thick`), `hair`, `dashed`, `dotted`, `double`, `mediumDashed`, `mediumDashDot`, `dashDot`, `dashDotDot`, `slantDashDot`. Canvas renderer paints distinct dash patterns via Canvas `setLineDash` for each variant                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Border "outline + inside" composite                                                        | ✅     | ❌      | ✅              | "All borders" option in the border picker — `changeBorder(..., "all", ...)` sets borders on every edge of every cell in the selection                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Merge cells                                                                                | ✅     | ✅      | ✅              | Standard + merge across rows                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| Clear formatting                                                                           | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Format painter / paint format                                                              | ✅     | ✅      | ✅              | Single + double-click toggle                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| User-defined color palette                                                                 | ✅     | ✅      | ✅              | `userDefinedColors` workbook setting                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| Workbook named cell styles (`<cellStyles>` — Heading 1, Accent N, Total, Good/Bad/Neutral) | ✅     | ❌      | ⚠️             | **Three layers, only two wired end-to-end:** (1) Toolkit round-trip preservation ✅ — parser reads `<cellStyles>` into `workbook.workbookStyles.namedCellStyles`; exporter accepts a `namedCellStyles` arg on `createExcelFile`. (2) Cell formatting from imported named styles ✅ — flows through the existing `cellStyleXfs` inheritance, so cells that reference a named style get the right font/fill on import. (3) UI gallery ⚠️ — `CellStyleSelector` ships with a hardcoded built-in gallery (Good/Bad/Neutral/Heading 1–4/Total/Title/20%/40%/60% Accent N) and accepts a `userDefinedStyles?: [string, CellFormat][]` prop, but nothing in `useSpreadsheetState` populates it from `workbook.workbookStyles.namedCellStyles` automatically — host-managed today. Cells store resolved formats (`uf`) rather than style refs; applying a named style snapshots its format onto each cell |
| Conditional formatting → cell style                                                        | ✅     | ✅      | ✅              | See Conditional Formatting section                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |

### Rows & columns

| Feature                          | Excel | Sheets | Rows n Columns | Notes                                                                                               |
| -------------------------------- | ----- | ------ | -------------- | --------------------------------------------------------------------------------------------------- |
| Insert / delete rows             | ✅     | ✅      | ✅              | Shifts dependent ranges + formulas                                                                  |
| Insert / delete columns          | ✅     | ✅      | ✅              |                                                                                                     |
| Insert / delete cells (shift)    | ✅     | ✅      | ✅              | Shift up/down/left/right                                                                            |
| Move rows / columns              | ✅     | ✅      | ✅              | Re-anchors conditional formats + validations                                                        |
| Resize rows / columns            | ✅     | ✅      | ✅              | Drag handle + auto-fit                                                                              |
| Hide / show rows / columns       | ✅     | ✅      | ✅              | Separate from group-collapse                                                                        |
| Freeze rows / columns (panes)    | ✅     | ✅      | ✅              |                                                                                                     |
| Split (non-frozen) panes         | ✅     | ❌      | ❌              | Deferred (Phase 9)                                                                                  |
| Group / ungroup rows / columns   | ✅     | ✅      | ✅              | 7 nesting levels; XLSX + ODS round-trip                                                             |
| Collapse / expand outline groups | ✅     | ✅      | ✅              | +/- gutter buttons, 1/2/3 level selector                                                            |
| Show row/column headers toggle   | ✅     | ✅      | ✅              | `showRowColHeaders` round-trips xlsx; canvas `showHeaders` prop honors it via `useSpreadsheetState` |
| Right-to-left layout             | ✅     | ✅      | ❌              | Deferred (Phase 9)                                                                                  |

### Formulas & functions

| Feature                                                                                       | Excel | Sheets | Rows n Columns | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| --------------------------------------------------------------------------------------------- | ----- | ------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Formula bar                                                                                   | ✅     | ✅      | ✅              | With Name Box + range selector                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Formula autocomplete                                                                          | ✅     | ✅      | ✅              | Function names + named ranges + table refs                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| Function tooltips (arg hints)                                                                 | ✅     | ✅      | ✅              | Per-arg highlight as you type                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| A1 cell references                                                                            | ✅     | ✅      | ✅              | Absolute (`$`) + relative                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Range references                                                                              | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Cross-sheet references                                                                        | ✅     | ✅      | ✅              | `Sheet1!A1`, `'Sheet 2'!A1`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Cross-workbook references                                                                     | ✅     | ✅      | ❌              | `[Book.xlsx]Sheet!A1` — deferred (Phase 6 remainder)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Named ranges (range-typed: `MyRange = A1:B10`)                                                | ✅     | ✅      | ✅              | Workbook + sheet scope                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Named ranges (static value: `MyVal = 42`)                                                     | ✅     | ✅      | ✅              | Scalar value returned directly                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Named **functions** (LAMBDA-typed: `MyFunc = =LAMBDA(x, x*2)`) called as `=MyFunc(args)`      | ✅     | ✅      | ✅              | Engine's `_callFunction` fallback resolves named LAMBDAs three ways: pre-built lambda value, `{ref, value}` wrapper, or the raw `"=LAMBDA(...)"` source string (parses + applies). Works in both the main-thread calc and the worker since both use the same fast-formula-parser engine. The named-range editor's "Text or Formula" type works out of the box                                                                                                                                                                                                                                                                                                                                         |
| Named ranges (formula-typed: `MyVar = =SUM(A1:A10)`) used as a value `=MyVar` (not as a call) | ✅     | ✅      | ✅              | Excel virtual-cell model: each formula-typed name is a first-class StaticNode (`nr:<scope>:<name>`) in the DAG. Cell formulas referencing the name get real DAG input edges to that node, so the unified topological sort recomputes them whenever the named range's value changes. Workbook + sheet scope are distinct (keyed by numeric `sheetId` so renames don't break things), lookup follows Excel rules (sheet-scoped first, workbook fallback). Order-independent registration via two-pass batch: `fullPrice = price * 1.08` wires the `nr:price → nr:fullprice` edge even if listed before `price`                                                                                          |
| Workbook & sheet-scoped names with the same display name                                      | ✅     | ✅      | ✅              | E.g. workbook `Tax = =0.08` and sheet-1 `Tax = =0.20` coexist as separate DAG nodes; resolved per Excel scoping (sheet first, then workbook)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| Chained named ranges (`fullPrice = price * 1.08`)                                             | ✅     | ✅      | ✅              | Real static-to-static DAG edge; unified topo sort evaluates `price` before `fullPrice` before any cell using `fullPrice`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Named-range edit in Name Manager (re-define formula)                                          | ✅     | ✅      | ✅              | Static node updated in place — dependent cells stay attached, are marked dirty, and recompute with the new value on next recalc                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Named range JSON round-trip (hydrate calc state from saved DAG)                               | ✅     | ❌      | ✅              | `Dag.toJSON` / `fromJSON` serialize both directions of the cell ↔ named-range edges so a rehydrated calculator matches the in-memory model                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| Structured table references                                                                   | ✅     | ❌      | ✅              | `Table1[#Headers]`, `Table1[@[Col 1]]`, totals row                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| \~378 Excel-compatible functions                                                              | ✅     | ✅      | ✅              | Math, statistical, text, date, financial, engineering, logical, info, lookup, web, etc.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| GETPIVOTDATA(value\_field, pivot\_anchor, \[field, item]…)                                    | ✅     | ✅      | ✅              | Header-matched lookup into a rendered pivot — locates the value cell for the given (field=item) tuple by scanning the pivot's row + column header bands                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| Dynamic arrays / spill                                                                        | ✅     | ✅      | ✅              | FILTER, SORT, SORTBY, UNIQUE, SEQUENCE, RANDARRAY                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `#SPILL!` / `#CALC!` / `#FIELD!` errors                                                       | ✅     | ✅      | ✅              | Modern error codes implemented                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `A1#` spilled-range operator                                                                  | ✅     | ❌      | ✅              | Lexer + grammar + eval + dep resolver all wired; resolves to the current spill bounds via Calculator.arrayRangeMap                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| Array formulas (legacy CSE)                                                                   | ✅     | ✅      | ✅              | `Ctrl+Shift+Enter`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| Modern array helpers (VSTACK, HSTACK, TOCOL, TOROW, etc.)                                     | ✅     | ✅      | ✅              | Phase 6 shipped                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Pick functions (CHOOSEROWS, CHOOSECOLS)                                                       | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Custom functions                                                                              | ✅     | ✅      | ✅              | Register via formula engine                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| LET                                                                                           | ✅     | ✅      | ✅              | Implemented via source-level expansion: `LET(name, value, ..., body)` inlines `(value)` for each name in the body before parse. Nested LET supported.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| LAMBDA (IIFE: `=LAMBDA(x, x*x)(5)`)                                                           | ✅     | ✅      | ✅              | Inlined at compile time via the LAMBDA preprocessor                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| LAMBDA as a value (passed to MAP/REDUCE/etc.)                                                 | ✅     | ✅      | ✅              | Standalone `LAMBDA(...)` transforms to `__RNC_LAMBDA__("p,...", "body source")` which constructs a runtime lambda value `{__isLambda, params, body}`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| MAP / REDUCE / BYROW / BYCOL / SCAN / MAKEARRAY                                               | ✅     | ✅      | ✅              | HOFs accept lambda values and re-invoke the parser per element with bindings substituted in. Recursion guard caps depth at 256                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Closures (lambda captures outer LET binding)                                                  | ✅     | ✅      | ✅              | LET expands before LAMBDA, baking captured values into the body source — e.g. `LET(n, 5, LAMBDA(x, x+n))` becomes `__RNC_LAMBDA__("x", "x+(5)")`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| LET-bound lambda used as a function (`LET(double, LAMBDA(x, x*2), double(5))`)                | ✅     | ✅      | ✅              | LET substitutes Function-shaped occurrences of binding names with the lambda source, then the IIFE inliner collapses it                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| Dep tracking through lambda bodies (`MAP(A1:A3, LAMBDA(v, v+B1))` tracks B1)                  | ✅     | ✅      | ✅              | DepParser walks `__RNC_LAMBDA__` body strings via a fresh inner parser instance                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Recursive lambdas (`LET(fact, LAMBDA(n, IF(n<=1, 1, n*fact(n-1))), fact(5))`)                 | ✅     | ✅      | ❌              | Body-reparse at runtime loses the LET binding for `fact`; needs a runtime registry that survives per-call re-parse. Deferred                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| GROUPBY / PIVOTBY                                                                             | ✅     | ❌      | ✅              | Built on the existing LAMBDA infra — `LambdaFunctions` entries + `funsNeedContext` registration only; no chevrotain grammar changes. GROUPBY buckets rows by row-field tuple and invokes the aggregation lambda once per group; PIVOTBY adds a column axis. Args supported: `field_headers`, `total_depth` (grand total top/bottom), `sort_order` (asc/desc), boolean `filter_array`. Aggregation lambdas receive the group's value vector as an Excel array literal `{a, b, c}` (already lexed natively), so any aggregator works — `LAMBDA(v, SUM(v))`, `LAMBDA(v, AVERAGE(v))`, custom LAMBDAs. Dep tracking through lambda bodies is automatic via the existing `__RNC_LAMBDA__` DepParser branch |
| FORECAST.ETS\* (Holt-Winters)                                                                 | ✅     | ❌      | ✅              | FORECAST.ETS / .CONFINT / .SEASONALITY / .STAT implemented via additive Holt-Winters (level + trend + seasonal). 5×5×5 grid search over α/β/γ to minimize in-sample SSE; seasonality auto-detected by comparing SSE across candidate periods 2..min(N/2, 12). Matches Excel within \~1% on typical seasonal series                                                                                                                                                                                                                                                                                                                                                                                    |
| Iterative calculation                                                                         | ✅     | ✅      | ✅              | enabled / iterations / delta parsed & honored                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| Manual / on-demand calculation                                                                | ✅     | ❌      | ✅              | Toggleable; queue-based dispatcher                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| Calculation in web worker                                                                     | ✅     | ✅      | ✅              | Off-main-thread engine via worker                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| Formula dependency tracing                                                                    | ✅     | ✅      | ✅              | Precedents / dependents (DAG-backed)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Formula auditing (highlight refs)                                                             | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Circular reference detection                                                                  | ✅     | ✅      | ✅              | Surfaces in calculation engine                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Spill-range blocking detection                                                                | ✅     | ✅      | ✅              | Emits `#SPILL!`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Formula protection (lock formulas)                                                            | ✅     | ✅      | ✅              | Per-cell `locked` + protection metadata                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| External / cross-workbook links                                                               | ✅     | ❌      | ⚠️             | Imported & cached; cross-workbook references can't be edited                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| Localized function names                                                                      | ✅     | ❌      | ❌              | Deferred (Phase 11); always English on the wire                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |

### Formula format derivation

When a formula cell has no explicit `uf.numberFormat`, the rendered format is auto-derived from the formula's structure and precedents — matching how Excel and Google Sheets infer the format of a calculated cell (e.g. `=TODAY()` → DATE, `=A1*B1` where A1 is currency → CURRENCY). Derivation runs at render time inside `getDerivedFormat` (`libs/spreadsheet-state/hooks/use-sheet-properties.ts`); results are cached per cell via an LRU keyed on `(value, precedent format refs)` so invalidation propagates whenever a direct precedent's format object identity changes.

| Feature                                                              | Excel | Sheets | Rows n Columns | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| -------------------------------------------------------------------- | ----- | ------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Function return-type defaults                                        | ✅     | ✅      | ✅              | \~140 functions: **DATE/TIME producers** — TODAY/NOW/DATE/TIME/EDATE/EOMONTH/**WORKDAY**/WORKDAY.INTL render as DATE/TIME/DATE\_TIME; **Date extractors** (DAY/MONTH/YEAR/HOUR/MINUTE/SECOND/WEEKDAY/WEEKNUM/ISOWEEKNUM/DATEDIF/DAYS/DAYS360/NETWORKDAYS/NETWORKDAYS.INTL/YEARFRAC/DATEVALUE/TIMEVALUE) render as NUMBER; **Math/trig** (POWER/SQRT/EXP/LN/LOG/LOG10/SIGN/SIN/COS/TAN/ASIN/ACOS/ATAN/ATAN2/SINH/COSH/TANH/asinh-acosh-atanh/DEGREES/RADIANS/PI/FACT/COMBIN/PERMUT/GCD/LCM/SUMSQ) → NUMBER; **Aggregation/rounding** (SUM/SUMPRODUCT/ROUND family/INT/MOD/QUOTIENT/CEILING/FLOOR/MROUND/TRUNC) → NUMBER with CURRENCY-precedent preservation; **Counts** (COUNT/COUNTA/COUNTIF/COUNTIFS/ROW/COLUMN/ROWS/COLUMNS/RANK/RANK.EQ/RANK.AVG/FREQUENCY/LEN/SEARCH/FIND/MATCH/XMATCH) → NUMBER; **Text** (TEXT/LEFT/RIGHT/MID/CONCAT/CONCATENATE/TEXTJOIN/LOWER/UPPER/PROPER/TRIM/CLEAN/SUBSTITUTE/REPLACE/REPT/ADDRESS/CHAR/UNICHAR/T/DOLLAR/FIXED/BAHTTEXT) → TEXT; **Statistical scalars** (AVEDEV/DEVSQ/KURT/SKEW/SKEW\.P/COVAR/COVARIANCE.S-P/CORREL/RSQ/SLOPE/INTERCEPT/STEYX/PEARSON) → NUMBER; **Financial rates** (XIRR/IRR/MIRR/RATE/NPER/EFFECT/NOMINAL) → NUMBER |
| Arg-index inheritance                                                | ✅     | ✅      | ✅              | SUMIFS/AVERAGEIFS/MINIFS/MAXIFS (sum\_range), SUMIF/AVERAGEIF (conditional 1 vs 3 args), FILTER/INDEX/SORT/SORTBY/UNIQUE/TAKE/DROP/TOCOL/TOROW/WRAPROWS/WRAPCOLS/CHOOSECOLS/CHOOSEROWS/TRANSPOSE, VLOOKUP (literal col), HLOOKUP (literal row), XLOOKUP (return\_array)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Statistical functions inherit format from data                       | ✅     | ✅      | ✅              | MEDIAN, MODE/MODE.SNGL/MODE.MULT, PERCENTILE/.INC/.EXC, QUARTILE/.INC/.EXC, LARGE, SMALL, TRIMMEAN, GEOMEAN, HARMEAN, AVERAGEA, MINA, MAXA, VAR/.S/.P/VARA/VARPA, STDEV/.S/.P/STDEVA/STDEVPA, FORECAST/.LINEAR/.ETS (y values arg)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Financial functions inherit CURRENCY from principal                  | ✅     | ✅      | ✅              | PMT/PV/FV (pmt or pv arg), NPV/XNPV (first value), IPMT/PPMT (pv), CUMIPMT/CUMPRINC (pv), DDB/SLN/SYD/DB (cost)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| Arithmetic format inference                                          | ✅     | ✅      | ✅              | AST-based `combineTypes` over ADD/SUB/MUL/DIV/EXP/CONCAT/COMPARE — DATE − DATE → NUMBER, DATE + N → DATE, %A × %B → NUMBER, CURRENCY × N → CURRENCY, A & B → TEXT, A = B → GENERAL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Conditional branch merge                                             | ✅     | ✅      | ✅              | IF, IFS, IFERROR, IFNA, CHOOSE, SWITCH — result-arg types merged via `mergeValueTypes`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ROUND family decimal adjustment                                      | ✅     | ✅      | ✅              | `ROUND(currency, 0)` shrinks the pattern's decimal count; ROUNDUP / ROUNDDOWN / TRUNC / INT / MROUND / CEILING / FLOOR / QUOTIENT covered                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| First-non-generic precedent wins (fold loop)                         | ✅     | ✅      | ✅              | NUMBER/GENERAL/COUNT are generic and get upgraded; once a specific format is locked in (PERCENT, CURRENCY, DATE, etc.) it stays — matches Excel's first-encountered semantics. AST aggregate handlers (SUM/AVG/MIN/MAX) still apply Excel's "currency wins" rule independently                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| CURRENCY pattern preservation across NUMBER coercion                 | ✅     | ✅      | ✅              | The `"$"#,##0.00` symbol+pattern survive when SUM / ROUND / etc. coerce the result type to NUMBER                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Date pattern fidelity through date-arithmetic functions              | ✅     | ✅      | ✅              | `=EDATE(A1, 1)` with A1's custom `yyyy-mm-dd` keeps `yyyy-mm-dd` instead of falling back to the locale default; same for EOMONTH, WORKDAY, etc. when the precedent is date-family                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Default patterns synthesized for date/time when no precedent has one | ✅     | ✅      | ✅              | `=TODAY()` → DATE with `getDefaultDateFormat(locale)`, `=NOW()` → DATE\_TIME, `=TIME(…)` → TIME (without these the renderer falls back to General and shows the serial number — the original `=TODAY()` bug)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Anchor-only range inheritance                                        | ✅     | ✅      | ✅              | `SUM(A1:A10)` inherits A1's format; O(1) per range, matches Excel's first-cell rule                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Style references (cellXfs resolution)                                | ✅     | ✅      | ✅              | Precedents stored as `{ sid }` style refs are resolved through the workbook `<xfs>` table before their `numberFormat` is read                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Multi-level chain propagation                                        | ✅     | ✅      | ✅              | `C1 = B1 = A1 = TODAY()` — all three render as DATE. Single-pass via cache fingerprint invalidation; no recursion through the dependency graph                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| Cycle safety in format derivation                                    | ✅     | ✅      | ✅              | The calculator rejects cycles; if one slips through (malformed graph during teardown) the cache-based lookup terminates without looping                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| OFFSET / INDIRECT / INDEX with computed indices / `A1#` spill refs   | ✅     | ✅      | ⚠️             | Falls back to the function's default return type — runtime-resolved targets aren't traced for format inheritance                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| AGGREGATE / SUBTOTAL function-number arg decoding                    | ✅     | ✅      | ❌              | First arg (`1`=AVG, `9`=SUM, …) selects the inner aggregator; we don't decode it, so format follows the precedent fold rather than the selected function's semantics                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Format derivation through LAMBDA / MAP / REDUCE bodies               | ✅     | ✅      | ⚠️             | Lambda bodies aren't re-introspected by the format inference; result format follows the precedent fold. Formula evaluation itself works (see Phase 7 rows above)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| Cross-workbook reference format inheritance                          | ✅     | ✅      | ❌              | Same scope as cross-workbook formula support — deferred with the rest of Phase 6                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| Multi-currency arithmetic (`$A + €B`)                                | ✅     | ✅      | ⚠️             | We follow Excel's general rule (first encountered for ADD/SUB, NUMBER across currencies for MUL/DIV); a few corner cases differ from Excel's exact behaviour                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

**Implementation guarantees:**

* **No user-space recursion through the cell dependency graph.** Earlier versions walked precedents recursively; deeply chained formulas could blow the JS stack. The current implementation is iterative — precedent lookups are O(1) cache reads, so chain depth is unbounded.
* **Single-pass cache convergence in the common case.** Row-major iteration naturally visits precedents before dependents, so `getEffectiveFormat` returns the correct format in one render pass. The LRU fingerprint catches any out-of-order resolution on the next render.
* **Per-call cost.** Cold-cache, single-precedent cell: \~2.5 μs. Warm-cache: \~140 ns. A 1000-cell render takes \~2.5 ms — well inside a 16 ms frame budget. Coverage measured by `libs/spreadsheet-state/hooks/__tests__/use-sheet-properties.bench.ts`.
* **Compatibility coverage.** \~96% of common Excel/Sheets format derivation cases. Remaining gaps: runtime-resolved references (OFFSET/INDIRECT), AGGREGATE/SUBTOTAL function-number decoding, format introspection through LAMBDA bodies, cross-workbook references — see the partial-support rows above.

### Data features

| Feature                                                                         | Excel | Sheets | Rows n Columns | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------- | ----- | ------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tables (structured data)                                                        | ✅     | ❌      | ✅              | Full `TableView` lifecycle                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Table styles + banded rows / columns                                            | ✅     | ❌      | ✅              | Header / total / first / last column overrides                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| Table totals row                                                                | ✅     | ❌      | ✅              | Per-column aggregation function preserved                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Custom table styles                                                             | ✅     | ❌      | ⚠️             | **Built-in styles fully wired; user-authored styles round-trip-only.** Built-in `TableStyleLight*` / `Medium*` / `Dark*` gallery ships in `TableStyleSelector` (theme-accent-derived), and a table picks one via `TableView.styleName`. User-authored XLSX `<tableStyle>` blocks round-trip preservation works — parser exposes them via `workbook.workbookStyles.getCustomTableStyleExports()`, exporter re-emits them via the `customTableStyles` arg on `createExcelFile` (each `<tableStyleElement>` re-registered as a dxf). What's NOT wired: the `TableStyleSelector` picker doesn't surface custom styles, `useSpreadsheetState` doesn't expose the list as reactive state, and applying a custom style by name to a `TableView` isn't a code path — host has to forward + render |
| AutoFilter (basic)                                                              | ✅     | ✅      | ✅              | Text, number, date, blank, error criteria                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Filter by color                                                                 | ✅     | ✅      | ✅              | Phase 3 shipped (XLSX + ODS round-trip)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Filter by icon                                                                  | ✅     | ❌      | ✅              | Phase 3 shipped                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Multi-column sort                                                               | ✅     | ✅      | ✅              | Multi-level **Sort dialog** (`SortRangeDialog`) — add / delete / reorder "then by" levels, each with column + sort-on (value / cell color / font color / icon) + order — opened from the AutoFilter dropdown's **Custom sort…**. Hooks accept multi-spec (`onSortRange` per-column `SortSpecs[]`, `onSortTable` multi-dimension overload). The context-menu range "Sort A→Z / Z→A" remains single-column                                                                                                                                                                                                                                                                                                                                                                                  |
| Sort by color / icon                                                            | ✅     | ❌      | ✅              | Selectable per level in the Sort dialog; color / icon swatches are auto-discovered per column (shared with the filter's discovery via `useColumnColorIcons`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Custom sort orders / lists                                                      | ✅     | ❌      | ✅              | Surfaced in the Sort dialog from `useSpreadsheetState({customAutofillLists})`; the chosen list drives `spec.customList`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Data validation                                                                 | ✅     | ✅      | ✅              | list, range, decimal, whole, date, time, textLength, custom formula. Commit-time **enforcement** (Excel `errorStyle`) — see the error/input messages row                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Data validation error/input messages                                            | ✅     | ✅      | ✅              | Authorable in the rule editor (input-message title/body + error-alert style/title/body) and **enforced on direct cell entry**: `stop` rejects the value (blocking modal, cell unchanged, focus stays); `warning` / `information` allow override (OK / Cancel). Input message renders as a cell tooltip. Paste / fill keep the flag-only red marker; `CUSTOM_FORMULA` rules fall back to flag-only (no synchronous eval at commit). The `<ValidationAlertDialog>` is host-mounted                                                                                                                                                                                                                                                                                                          |
| Dropdown lists from range                                                       | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Dropdown lists (inline values)                                                  | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Schema-based tables (typed columns)                                             | ❌     | ❌      | ✅              | Custom — not an Excel feature                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Calculated columns                                                              | ✅     | ❌      | ✅              | Auto-fill formula on insert / paste                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Pivot tables (rendering imported)                                               | ✅     | ✅      | ✅              | XLSX `pivotCacheDefinition` + `pivotTable` round-trip; rendered through the same `libs/pivot` pipeline as authored pivots                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Pivot tables (authoring UI)                                                     | ✅     | ✅      | ✅              | DuckDB-powered analytical layer (PIVOT operator + JS post-transforms)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| Pivot row / column / value fields (drag, reorder, show-hide)                    | ✅     | ✅      | ✅              | Drag-and-drop field list panel; per-field hide checkbox in editor                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| Pivot subtotals + grand totals toggles                                          | ✅     | ✅      | ✅              | Per-pivot `showSubtotals` + `showRowGrandTotals` + `showColumnGrandTotals` (`PivotEditor`'s Totals panel)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Pivot sort (row label, column label, value field)                               | ✅     | ✅      | ✅              | Tri-state ↑/↓/none on every field; value-field sort handles BOTH the grouping-only alias shape (`"Field"`) and the column-pivoted shape (`agg(Field)`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| Pivot label filter (set values, search, select-all)                             | ✅     | ✅      | ✅              | Per-field filter popover with searchable checkbox list                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| Pivot Top N filter                                                              | ✅     | ✅      | ✅              | Top / Bottom × N by any value field — emits `IN (SELECT ... ORDER BY agg(byField) LIMIT N)` against the SOURCE CTE                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Pivot refresh button + auto-refresh on source mutation                          | ✅     | ✅      | ✅              | Manual refresh icon in `PivotEditor`; `sourceDataVersion` invalidates results when source range mutates                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Pivot drilldown (double-click value cell)                                       | ✅     | ✅      | ✅              | `onDrillDownAtCell` returns the underlying source rows for the clicked aggregate                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| Pivot "Show Values As" (% of total, % of row, % of column, running total, rank) | ✅     | ✅      | ✅              | Per value field — post-aggregation JS transform; PERCENT / NUMBER number-format derived from the chosen mode                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Pivot field grouping (date by period, numeric by bucket size)                   | ✅     | ✅      | ✅              | DuckDB EXCLUDE-based SOURCE rewrite (`date_trunc` / `FLOOR`) — period whitelisted to prevent SQL injection                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| GETPIVOTDATA()                                                                  | ✅     | ✅      | ✅              | 2D matrix header-matching lookup; resolves the value cell for a given (field=value, …) tuple                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Slicer ↔ pivot wiring                                                           | ✅     | ❌      | ✅              | Slicer selection applies as a label filter to bound pivots; pivot re-runs through the same filter pipeline                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Pivot tables (XLSX round-trip of cache+def)                                     | ✅     | ❌      | ✅              | `refreshOnLoad="1"` so Excel rebuilds the cache from the live source; positions, rows/columns/values, aggregation function, sort + filter state all preserved                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Calculated fields / calculated items                                            | ✅     | ❌      | ❌              | Deferred (Phase 10)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Slicers (table-backed)                                                          | ✅     | ❌      | ✅              | Create/move/resize/delete + XLSX + ODS round-trip                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| Slicers (pivot-backed)                                                          | ✅     | ❌      | ✅              | Slicer reads distinct values from the bound pivot's source range (column matching `fieldName`); selection routes through `onFilterPivot` → `applySlicerSelectionToPivots` → pivot's filter pipeline. XLSX export of the pivot OLAP cube cache itself deferred — table-backed slicers ship in full                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| Timeline filters                                                                | ✅     | ❌      | ❌              | Deferred                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |

### Conditional formatting

| Feature                                 | Excel | Sheets | Rows n Columns | Notes                                                                          |
| --------------------------------------- | ----- | ------ | -------------- | ------------------------------------------------------------------------------ |
| Number-based rules                      | ✅     | ✅      | ✅              | greater / less / equal / between, etc.                                         |
| Text-based rules                        | ✅     | ✅      | ✅              | contains / starts with / ends with / equals                                    |
| Date-based rules                        | ✅     | ✅      | ✅              | today, yesterday, this week, etc.                                              |
| Custom formula rules                    | ✅     | ✅      | ✅              |                                                                                |
| Gradient / color scale (2-stop, 3-stop) | ✅     | ✅      | ✅              |                                                                                |
| Data bars                               | ✅     | ❌      | ✅              | `dataBarRule` round-trips XLSX + ODS (see `cf-roundtrip.spec.ts`)              |
| Icon sets                               | ✅     | ❌      | ✅              | `iconSetRule` round-trips XLSX + ODS (3TrafficLights1, 3Arrows, 5Rating, etc.) |
| Top / bottom N rules                    | ✅     | ❌      | ⚠️             | Implementable via custom formula                                               |
| Duplicate / unique rules                | ✅     | ❌      | ✅              |                                                                                |
| Range re-anchor on insert/move          | ✅     | ✅      | ✅              | `updateRangedRulesOnStructuralChange` + `updateRangedRulesOnReorder`           |

### Charts

| Feature                                                  | Excel | Sheets | Rows n Columns | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| -------------------------------------------------------- | ----- | ------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Column charts (clustered / stacked)                      | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Bar charts                                               | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Line charts (smooth / stepped)                           | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Area charts                                              | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Pie / doughnut charts                                    | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Scatter charts                                           | ✅     | ✅      | ✅              | `c:scatterChart` import — each series carries `c:xVal`/`c:yVal` instead of the `c:cat`/`c:val` shape used by other charts. Export emits the same. Renders via plotly/echarts scatter                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| Bubble charts                                            | ✅     | ✅      | ✅              | `c:bubbleChart` — adds `c:bubbleSize` (third dimension). Bubble size scales calculated via `createBubbleSizeScale`. Round-trip preserves all three axes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Pie of Pie / Bar of Pie (`c:ofPieChart`)                 | ✅     | ❌      | ⚠️             | Renders as a plain pie — we surface the data and slices but don't reconstruct the secondary slice expansion. Round-trips through the pie path                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Cross-sheet data references                              | ✅     | ✅      | ✅              | A chart on Sheet1 can pull its data from `'Other Sheet'!$A$1:$A$10`. Parser resolves the prefix to the actual data sheet's sheetId so every series source is pinned correctly; exporter re-encodes the sheet name                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| Radar charts                                             | ✅     | ❌      | ✅              | Creatable from the chart-type picker (standard / with-markers / filled) and rendered natively — echarts `radar` coordinate + plotly `scatterpolar` (`utils/radar.ts`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| Treemap                                                  | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Sunburst                                                 | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Stock (line-derived)                                     | ✅     | ❌      | ✅              | Creatable from the picker (HLC / OHLC) and rendered as line series in echarts + plotly (`utils/stock.ts` `toLineSeries`); a `toCandlestick` transform is also available for renderers that prefer OHLC bodies                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Stock (OHLC / candlestick)                               | ✅     | ❌      | ✅              | OHLC / VOHLC variants render as a native candlestick — echarts `candlestick` series + plotly `candlestick` trace (`utils/stock.ts` `toCandlestick`). Pickable from the chart-type picker (Stock → OHLC)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Waterfall                                                | ✅     | ❌      | ✅              | Renders in echarts + plotly. Phase 5.5b: full c15:waterfallChart in mc:Choice + c:barChart fallback — Excel 2016+ renders natively                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Funnel                                                   | ✅     | ❌      | ✅              | Renders in echarts + plotly. Phase 5.5b: full c15:funnelChart in mc:Choice + c:barChart fallback — Excel 2016+ renders natively                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 3D variants (bar3D / column3D / pie3D / line3D / area3D) | ✅     | ❌      | ❌              | Deferred — needs `echarts-gl` + plotly mesh3d construction                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Surface                                                  | ✅     | ❌      | ❌              | Deferred (same dep as 3D)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Histogram                                                | ✅     | ✅      | ✅              | Native histogram (echarts/plotly) with auto/Sturges binning + bin-count override. Phase 5.5b: exporter emits full c15:histogramChart in mc:Choice with c:barChart fallback — Excel 2016+ renders natively                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Box & whisker                                            | ✅     | ❌      | ✅              | Native boxplot (echarts boxplot series, plotly box trace). Phase 5.5b: full c15:boxWhiskerChart (quartileMethod, showMean, showOutlier) in mc:Choice + c:barChart fallback — Excel 2016+ renders natively                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Pareto                                                   | ✅     | ❌      | ⚠️             | Constructible via column + line combo                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| Combo (mixed series types)                               | ✅     | ✅      | ✅              | Per-series `seriesChartType` picker in chart-editor (bar/column/line/area)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Dual-axis (secondary axis)                               | ✅     | ✅      | ✅              | Per-series **Plot on secondary axis** checkbox in the chart editor; rendered as a second yAxis in echarts (`yAxisIndex`) and `yaxis2` in plotly. `secondaryValueAxisOptions` round-trips; the per-series flag is editor-set (the XLSX parser doesn't tag series by axis)                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Chart titles, legends, axis labels                       | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Data labels (value / category / percent)                 | ✅     | ✅      | ✅              | Rendered from spec **and authorable** in the chart editor — master "Show data labels" toggle + value / category name / series name / percentage checkboxes + label position (center / inside / outside / inside-end / outside-end / best-fit)                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Trendlines                                               | ✅     | ✅      | ✅              | linear / polynomial / exponential / logarithmic (+ power, moving-avg). Per-series toggle + type picker in the chart editor; regression computed in `utils/trendline.ts` (with R² + equation) and drawn as an overlay line in both echarts + plotly                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Error bars                                               | ✅     | ✅      | ✅              | Per-series toggle (fixedVal / percentage / stdDev / stdErr) in the editor; magnitudes from `utils/error-bars.ts`, drawn via an echarts custom series + plotly native `error_y`. `custom` range-based bars are unsupported (the model carries no plus/minus range fields)                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Chart styles / colors                                    | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Chart series gradient fill                               | ✅     | ✅      | ✅              | Per-series gradient toggle in the chart editor. **Rendered**: echarts paints faithfully via `graphic.LinearGradient` (cardinal angles axis-aligned, `utils/gradient.ts`); plotly degrades to a representative solid color (no per-trace gradient fill). Linear (degree) + path (left/right/top/bottom insets). XLSX exporter emits `<a:gradFill>` inside `<c:spPr>` with EMU-per-mille positions + 60000ths-of-a-degree angle. Parser hydrates from the same shape. ODS exporter emits `<draw:gradient>` defs in automatic-styles + references via `draw:fill-gradient-name` on the series style; ODS-side parser hydration is deferred (rare in the wild). Field on `ChartData.gradient` |
| Chart move / resize                                      | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Chart editing UI                                         | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Formula-driven titles & labels                           | ✅     | ✅      | ✅              | Range references resolved at render                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |

### Visual content

| Feature                                                          | Excel | Sheets | Rows n Columns | Notes                                                                                                                                                                                                                                                                                                             |
| ---------------------------------------------------------------- | ----- | ------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Embedded images                                                  | ✅     | ✅      | ✅              | Insert, move, resize, replace                                                                                                                                                                                                                                                                                     |
| Image properties (brightness/contrast/transparency/crop/z-order) | ✅     | ⚠️     | ❌              | Deferred (Phase 8); only position/size round-trips                                                                                                                                                                                                                                                                |
| Shapes (rectangles, lines, arrows, text boxes)                   | ✅     | ✅      | ⚠️             | Imported; export deferred (Phase 8)                                                                                                                                                                                                                                                                               |
| SmartArt                                                         | ✅     | ❌      | ❌              | Out of scope                                                                                                                                                                                                                                                                                                      |
| Sparklines — line / column / win-loss                            | ✅     | ✅      | ✅              | XLSX + ODS round-trip. Axis options round-trip too: `displayEmptyCellsAs` (gap/zero/span), `dateAxis`, `markers`, `high`/`low`/`first`/`last`/`negative` point highlights, `displayXAxis`, `lineWeight`, `manualMax`/`manualMin`, `minAxisType`/`maxAxisType` — all carried through `SPARKLINE()` formula options |
| Hyperlinks (URL + tooltip)                                       | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                   |
| Internal links (`Sheet!A1`)                                      | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                   |
| Comments (legacy)                                                | ✅     | ❌      | ✅              | Author + rich text                                                                                                                                                                                                                                                                                                |
| Comments (threaded with replies)                                 | ✅     | ✅      | ❌              | Tier 1 blocker — `commentsThreaded.xml` not parsed                                                                                                                                                                                                                                                                |
| Citations / inline annotations                                   | ❌     | ❌      | ✅              | Custom — surfaces inside `=FILTER` source attribution & similar                                                                                                                                                                                                                                                   |
| Mentions (`@user`)                                               | ❌     | ✅      | ✅              | Mentions component bundled                                                                                                                                                                                                                                                                                        |
| Cell tooltips / popovers                                         | ✅     | ✅      | ✅              | Programmatic via `getCellPopoverContent`                                                                                                                                                                                                                                                                          |
| Cell renderers (custom)                                          | ❌     | ❌      | ✅              | Custom — render React components inside cells                                                                                                                                                                                                                                                                     |
| Structured cell renderers                                        | ❌     | ❌      | ✅              | Custom — schema-driven rendering                                                                                                                                                                                                                                                                                  |

### Workbook structure

| Feature                                       | Excel | Sheets | Rows n Columns | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| --------------------------------------------- | ----- | ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Multiple sheets                               | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Sheet tabs UI                                 | ✅     | ✅      | ✅              | Drag-to-reorder                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Sheet rename                                  | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Sheet duplicate                               | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Sheet move / reorder                          | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Sheet color                                   | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Hide / show sheets                            | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Very-hidden sheets                            | ✅     | ❌      | ✅              | XLSX `state="veryHidden"` preserved                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Sheet protection (cell-level locked metadata) | ✅     | ✅      | ✅              | Preserved on round-trip                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| Sheet protection (enforcement)                | ✅     | ✅      | ✅              | Every mutation hook in `libs/spreadsheet-state/hooks/` gates through `useCanEditCellRange` / `useCanEditSheet`. The host provides the policy via `SpreadsheetContext.canEditCell` + `isSheetProtected` (defaults are permissive — protection metadata round-trips but only gets enforced when the host wires a policy). Gates cover: single + batch cell edits, paste, fill, delete cells / rows / columns, insert cells / rows / columns, move, merge / unmerge, sort (range + table), filter table, change formatting, change decimals, change border, clear formatting, paint format, hide / show rows + columns, group / ungroup, resize, conditional-formatting CRUD, data-validation CRUD, named-range create + update. Streamed batch writes are gated up front before the stream starts. |
| Workbook protection                           | ✅     | ✅      | ✅              | Structural-protection gate (`useCanEditWorkbook`) refuses add / delete / rename / move / duplicate / hide / show / tab-color when the host sets `isWorkbookProtected={true}` on the canvas grid (matches Excel `workbookProtection lockStructure="1"`). Cell edits remain governed by per-sheet protection.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| Named ranges                                  | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| External links / connections                  | ✅     | ❌      | ⚠️             | Read-only; not actively refreshed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Themes (light / dark)                         | ✅     | ❌      | ✅              | Light/dark variants generated from workbook theme                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Custom themes                                 | ✅     | ❌      | ⚠️             | Colors preserved; full theme structure simplified                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Zoom / scale                                  | ✅     | ✅      | ✅              | Per-sheet `zoomScale` (10–400) round-trips xlsx; exposed from `useSpreadsheetState`; canvas `scale` prop already supports the full 25–400% range via Ctrl+wheel + `ScaleSelector`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Grid lines toggle                             | ✅     | ✅      | ✅              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Print setup (page setup, headers, footers)    | ✅     | ✅      | ⚠️             | Print preview dialog (`PrintPreviewDialog`) ships: paper size (Letter / A4 / Legal / Tabloid), portrait/landscape, scale (Normal / Fit width / Fit page / Custom %), margins (Normal / Narrow / Wide / Custom), horizontal + vertical alignment, show-gridlines, scope (Current sheet / Workbook / Selection), Cmd/Ctrl+P shortcut, paginated previews + iframe-driven `window.print()` with `@page` rules. Headers/footers template language (`&L`/`&C`/`&R` zones, `&P`/`&N`/`&D` macros) and repeat-frozen-rows/cols on each printed page are the remaining gap.                                                                                                                                                                                                                              |
| Page breaks                                   | ✅     | ✅      | ⚠️             | Auto-pagination renders correctly in the print preview (greedy fit, down-then-over page order — same as Excel's `pageOrder = "downThenOver"` default). Drag-to-edit custom page breaks is the remaining gap; the underlying `<rowBreaks>` / `<colBreaks>` XLSX metadata still round-trips.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Multi-instance spreadsheets on one page       | ❌     | ❌      | ✅              | Custom — `instanceId` isolates state                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Sheet switcher (jump-to-sheet)                | ❌     | ❌      | ✅              | Component bundled                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |

### Collaboration

| Feature                         | Excel      | Sheets | Rows n Columns | Notes                                                                                |
| ------------------------------- | ---------- | ------ | -------------- | ------------------------------------------------------------------------------------ |
| Real-time multi-user editing    | ✅ (online) | ✅      | ✅              | Backends: YJS, ShareDB, Supabase Realtime                                            |
| User presence / avatars         | ✅          | ✅      | ✅              | Per-cell active-user overlay                                                         |
| Cursor / selection broadcast    | ✅          | ✅      | ✅              |                                                                                      |
| Conflict resolution (OT / CRDT) | ✅          | ✅      | ✅              | OT (ShareDB) + CRDT (YJS) options                                                    |
| Comments threading              | ✅          | ✅      | ❌              | (Same gap as in Visual content section)                                              |
| Version history                 | ✅          | ✅      | ⚠️             | Diff via `libs/version-comparison`; full version-history UI is host's responsibility |
| @mentions in cells              | ❌          | ✅      | ✅              |                                                                                      |

### Import / export — detailed

#### XLSX import

| Bucket                                                                                                            | Status | Notes                                                                                                                                                                                                   |
| ----------------------------------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cell values, types, formulas                                                                                      | ✅      | Including shared strings, inline strings, rich text runs                                                                                                                                                |
| Cell formatting (fonts, fills, borders, alignment, number formats)                                                | ✅      | Theme tints resolved; \~68 built-in number formats                                                                                                                                                      |
| Merged cells                                                                                                      | ✅      |                                                                                                                                                                                                         |
| Hyperlinks (with tooltips)                                                                                        | ✅      |                                                                                                                                                                                                         |
| Comments (legacy `comments.xml`)                                                                                  | ✅      | Threaded `commentsThreaded.xml` not parsed                                                                                                                                                              |
| Hidden / very-hidden sheets                                                                                       | ✅      |                                                                                                                                                                                                         |
| Row heights / column widths                                                                                       | ✅      |                                                                                                                                                                                                         |
| Hidden rows / columns                                                                                             | ✅      | Separates `hiddenByUser` vs `hiddenByGroup`                                                                                                                                                             |
| Outline / grouping (rows + cols, nested)                                                                          | ✅      | `outlineLevel`, `collapsed`, `outlinePr` summary direction                                                                                                                                              |
| Freeze panes                                                                                                      | ✅      |                                                                                                                                                                                                         |
| Tab color                                                                                                         | ✅      |                                                                                                                                                                                                         |
| Named ranges (workbook + sheet scope)                                                                             | ✅      |                                                                                                                                                                                                         |
| Tables (with totals row, banded styling)                                                                          | ✅      |                                                                                                                                                                                                         |
| AutoFilter (text, number, date, color, icon, blank, error)                                                        | ✅      |                                                                                                                                                                                                         |
| Data validation (all 9 rule types)                                                                                | ✅      |                                                                                                                                                                                                         |
| Conditional formatting (boolean, gradient, data bars, icon sets, custom formula)                                  | ✅      | All rule types round-trip                                                                                                                                                                               |
| Charts (column / bar / line / area / pie / doughnut / scatter / bubble / treemap / sunburst / radar / stock-line) | ✅      |                                                                                                                                                                                                         |
| Charts (histogram / box & whisker / waterfall / funnel)                                                           | ✅      | Parser detects both the new mc:AlternateContent + c15 element shape (what we now emit and what Excel emits) and the legacy c:extLst marker (for backwards compat with files we wrote before Phase 5.5b) |
| Charts (3D / surface)                                                                                             | ❌      | Deferred                                                                                                                                                                                                |
| Embedded images + drawings                                                                                        | ✅      | Position & size only                                                                                                                                                                                    |
| Shapes (rectangles, lines, arrows, text boxes)                                                                    | ⚠️     | Read; export deferred                                                                                                                                                                                   |
| Sparklines                                                                                                        | ✅      |                                                                                                                                                                                                         |
| Slicers (table-backed)                                                                                            | ✅      | Pivot slicers not yet                                                                                                                                                                                   |
| Pivot tables                                                                                                      | ✅      | Parses `xl/pivotCache/*` + `xl/pivotTables/*` — A1 source range, sheet prefix stripped, agg-fn normalized, sort + filter state hydrated                                                                 |
| Themes (colors + fonts)                                                                                           | ✅      | Light + dark variants generated                                                                                                                                                                         |
| Iterative calculation settings                                                                                    | ✅      |                                                                                                                                                                                                         |
| External links / connections                                                                                      | ⚠️     | Cached; not refreshed                                                                                                                                                                                   |
| Macros / VBA                                                                                                      | ❌      | Discarded for security                                                                                                                                                                                  |
| ActiveX / OLE / linked data types / SmartArt / Cube functions                                                     | ❌      | Out of scope                                                                                                                                                                                            |
| Password-encrypted file decryption                                                                                | ❌      | Not implemented                                                                                                                                                                                         |

#### XLSX export

| Bucket                                                                                                            | Status              | Notes                                                                                                                                                                                                                                                                                                   |
| ----------------------------------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cell values, formulas, shared strings, inline strings                                                             | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Full formatting (fonts/fills/borders/align/number formats)                                                        | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Merged cells                                                                                                      | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Hyperlinks                                                                                                        | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Comments (legacy)                                                                                                 | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Hidden / very-hidden sheets                                                                                       | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Row heights / column widths                                                                                       | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Hidden rows / columns                                                                                             | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Outline / grouping (rows + cols, nested)                                                                          | ✅                   | `outlinePr` only emitted when non-default                                                                                                                                                                                                                                                               |
| Freeze panes                                                                                                      | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Tab color                                                                                                         | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Named ranges (workbook + sheet scope)                                                                             | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Tables (with totals row + banded styling)                                                                         | ✅                   |                                                                                                                                                                                                                                                                                                         |
| AutoFilter (incl. color + icon criteria)                                                                          | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Data validation                                                                                                   | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Conditional formatting (boolean, gradient, data bars, icon sets, custom formula)                                  | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Charts (column / bar / line / area / pie / doughnut / scatter / bubble / treemap / sunburst / radar / stock-line) | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Charts (histogram / box & whisker / waterfall / funnel)                                                           | ✅                   | Emitted as `mc:AlternateContent` with full `c15:histogramChart`/`c15:boxWhiskerChart`/`c15:waterfallChart`/`c15:funnelChart` in `mc:Choice` (Excel 2016+ renders natively) plus a `c:barChart` in `mc:Fallback` for older Excel                                                                         |
| Embedded images                                                                                                   | ✅                   | Base64 + relationship wired                                                                                                                                                                                                                                                                             |
| Sparklines                                                                                                        | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Slicers (table-backed)                                                                                            | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Themes (colors + fonts)                                                                                           | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Iterative calculation settings                                                                                    | ✅                   |                                                                                                                                                                                                                                                                                                         |
| Password encryption (CFB)                                                                                         | ✅                   | Optional `password` arg in `createExcelFile`                                                                                                                                                                                                                                                            |
| Shapes export                                                                                                     | ❌                   | Deferred                                                                                                                                                                                                                                                                                                |
| Pivot table cache + definition                                                                                    | ✅                   | Emits `xl/pivotCache/pivotCacheDefinitionN.xml` + `xl/pivotTables/pivotTableN.xml` with `refreshOnLoad="1"`; workbook `<pivotCaches>` and per-worksheet relationships wired                                                                                                                             |
| Pivot slicer cache (OLAP)                                                                                         | ❌                   | Deferred — `buildSlicerCacheXml()` currently returns null for pivot-backed slicers (table-backed slicers ship)                                                                                                                                                                                          |
| Custom table styles                                                                                               | ✅ (round-trip only) | `<cellStyles>` + `<tableStyles>` blocks emitted from `customTableStyles` / `namedCellStyles` args on `createExcelFile`; element formats re-registered as dxfs. Toolkit-level preservation only — the in-app pickers (`TableStyleSelector` / `CellStyleSelector`) don't surface user-authored styles yet |
| Real-time collab metadata                                                                                         | ❌                   | Spreadsheet-internal; intentionally not emitted                                                                                                                                                                                                                                                         |

#### ODS import

Identical bucket coverage as XLSX import. ODS-specific notes:

| Item                                                               | Status | Notes                                                    |
| ------------------------------------------------------------------ | ------ | -------------------------------------------------------- |
| Outline (`<table:table-row-group>` / `<table:table-column-group>`) | ✅      | `table:display="false"` → collapsed; both axes & nested  |
| Sparklines (LibreOffice extension)                                 | ✅      | `calcext:sparkline-groups` parsed                        |
| Database ranges (ODS's AutoFilter equivalent)                      | ✅      |                                                          |
| Conditional formatting (LibreOffice extension)                     | ✅      |                                                          |
| Data validation                                                    | ✅      |                                                          |
| Charts                                                             | ✅      |                                                          |
| Sheet protection metadata                                          | ⚠️     | Parsed; same enforcement gap as XLSX                     |
| Pivot tables                                                       | ⚠️     | Structure parsed; same Phase 10 gap as XLSX              |
| `outlinePr` direction                                              | ⚠️     | ODS has no native equivalent; defaults applied on import |

#### ODS export

| Item                                                    | Status | Notes                                                                                        |
| ------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------- |
| All formatting, formulas, sheet structure               | ✅      |                                                                                              |
| Outline groups                                          | ✅      | Nested `<table:table-row-group>` / `<table:table-column-group>` with `table:display="false"` |
| Tables → database ranges                                | ✅      |                                                                                              |
| Sparklines                                              | ✅      | LibreOffice extension element emitted                                                        |
| Charts                                                  | ✅      |                                                                                              |
| Slicers (table-backed)                                  | ✅      | LibreOffice subset                                                                           |
| Images                                                  | ✅      |                                                                                              |
| `outlinePr.summaryBelow` / `summaryRight` (non-default) | ❌      | Not persisted — ODS has no equivalent attribute                                              |

#### CSV import / export

| Feature                              | Import | Export | Notes                                                      |
| ------------------------------------ | ------ | ------ | ---------------------------------------------------------- |
| Plain values                         | ✅      | ✅      | Type inferred on import (number / date / boolean / string) |
| Custom delimiter                     | ✅      | ✅      | Comma, semicolon, tab, custom                              |
| Quote escaping                       | ✅      | ✅      | RFC 4180                                                   |
| Encoding (UTF-8, UTF-16)             | ✅      | ✅      | BOM detection                                              |
| Multi-sheet                          | ❌      | ❌      | CSV is single-sheet by spec                                |
| Formatting / formulas                | ❌      | ❌      | Lost on round-trip                                         |
| Worker-based parsing for large files | ✅      | N/A    |                                                            |

## Engineering capabilities (developer-facing)

These are features that don't have an Excel equivalent — they're framework / SDK capabilities:

| Capability                                  | Description                                                     |
| ------------------------------------------- | --------------------------------------------------------------- |
| Headless UI mode                            | Components render without forcing a specific UI shell           |
| `useSpreadsheetState` hook                  | Single hook that wires every callback + state slice             |
| Imperative spreadsheet API                  | Programmatic access via ref handle (set value, get range, etc.) |
| Multi-instance spreadsheets                 | Multiple independent grids on one page via `instanceId`         |
| Calculate on-demand                         | Toggle automatic recalculation; queue dispatcher exposed        |
| Real-time data binding                      | Stream external values into cells (e.g. live ticker)            |
| Web-worker calculation                      | Off-main-thread formula engine                                  |
| Custom cell editors                         | Replace per-type editor (e.g. richer date picker)               |
| Custom cell renderers                       | Replace per-cell paint with React component                     |
| Structured cell renderer                    | Schema-driven cell rendering                                    |
| Custom formula evaluation                   | Inject custom function implementations                          |
| Tokenizer access                            | Parse formulas independently of the engine                      |
| Cell format registry                        | Register custom number formats globally                         |
| Shared strings management                   | Manual control of the workbook's interned string pool           |
| Mentions                                    | `@user` mention engine bundled                                  |
| Localisation                                | i18n for UI strings (formula names still English)               |
| Lazy loading / infinite scrolling           | Render only what's visible; load rows on demand                 |
| Export canvas as image                      | Serialize current viewport to PNG                               |
| OpenAI / ChatGPT integration                | Inline AI prompt + apply (magic fill, formula generation)       |
| Drag and drop (files)                       | Drop CSV / XLSX onto the canvas to import                       |
| Insert date/time helpers                    | Toolbar action                                                  |
| Insert link / image editor components       | Bundled                                                         |
| Navigate to range / named range editor      | Bundled components                                              |
| Sheet status component                      | Pre-built bottom-bar status widget                              |
| Range selector / selection input components | Reusable picker controls                                        |
| Version comparison                          | Diff two snapshots and surface changes                          |

## Currently unsupported (deferred)

This list mirrors `docs/compat/remaining-gaps.md`. Tier 1 items can silently lose data on round-trip:

**Tier 1 — data-loss blockers:**

* Sheet protection enforcement ✅ shipped — `useCanEditCellRange` / `useCanEditSheet` gates threaded through every mutation hook; host provides the policy via `SpreadsheetContext.canEditCell` + `isSheetProtected`
* Workbook protection enforcement ✅ shipped — `useCanEditWorkbook` gates structural sheet ops (add / delete / rename / move / duplicate / hide / show / tab-color); host opts in via `isWorkbookProtected={true}` on the canvas grid
* Threaded comments (with replies, resolved state)

**Phase 6 (formula) remainder:**

* Cross-workbook references `[Book.xlsx]Sheet!A1`
* GROUPBY / PIVOTBY ✅ shipped — `LambdaFunctions` entries built on top of the existing LAMBDA infra. Tokenizer-aware substitution + Excel-native `{a,b,c}` array literals; no chevrotain grammar changes
* FORECAST.ETS\* family

**Phase 7 — LAMBDA family:**

* LET ✅ shipped (source-level expansion)
* LAMBDA + MAP / REDUCE / BYROW / BYCOL / SCAN / MAKEARRAY ✅ shipped (IIFE inlining + runtime lambda values via `__RNC_LAMBDA__` preprocessor; HOFs registered in `funsNeedContext` and re-invoke the parser per element)
* LET-bound lambdas callable like functions ✅ shipped (Function-token substitution in `substituteNamesInBody`)
* Closures via LET capture ✅ shipped (LET runs first, bakes values into lambda body strings)
* Workbook-named LAMBDAs callable as `=MyFunc(args)` ✅ shipped — `_callFunction` resolves via `onVariable` and accepts three shapes: pre-built lambda value, `{ref, value}` wrapper, or raw `"=LAMBDA(...)"` source string (parsed + applied). Works for both the main-thread calc and the worker since both use the rebuilt parser dist
* Dep parser walks lambda body strings ✅ shipped (`__RNC_LAMBDA__` branch in DepParser.callFunction uses a fresh inner instance)
* Recursive lambdas (e.g. `LET(fact, LAMBDA(n, IF(...)), fact(5))`) ❌ deferred — body-reparse at runtime loses the LET binding; needs a runtime registry that survives the per-call re-parse
* Named-range formula evaluation (general — `=MyVar` where MyVar = `=SUM(A1:A10)`) ✅ shipped — Excel virtual-cell model with real DAG edges, unified topological sort, scope-aware (workbook + per- sheet) keys, order-independent batch registration, and JSON round-trip. See "Formulas & functions" table above for the user- visible rows.

**Phase 8 — visual fidelity:**

* Image brightness / contrast / transparency / z-order / cropping
* Shape export (rectangles, lines, arrows, text boxes)
* Gradient cell fills ✅ shipped — `<gradientFill>` round-trip + canvas paint (linear + path)

**Phase 9 — view & UX:**

* RTL layout
* Non-frozen pane split

**Phase 10 — pivot tables:**

* Full XLSX cache + definition round-trip ✅ shipped — import (`pivots.ts`) + export (`pivot-builder.ts`); refreshOnLoad rebuilds cache from live source
* Authoring UI (drag rows/cols/values, hide fields, subtotals + grand-totals toggles, sort, label filter, Top N, refresh, drilldown) ✅ shipped
* Show Values As (% of total / row / column, running total, rank ↑/↓) ✅ shipped — PERCENT/NUMBER number-format applied to value + total cells
* Field grouping (date by period, numeric by bucket) ✅ shipped via DuckDB EXCLUDE-based SOURCE rewrite
* GETPIVOTDATA() ✅ shipped
* Slicer ↔ pivot wiring ✅ shipped (label-filter pipeline)
* Calculated fields / calculated items ❌ deferred
* Pivot-backed slicer OLAP cube cache export ❌ deferred (table-backed slicers ship)
* Pivot table styles (PivotStyleLight/Medium/Dark exporter mapping) ❌ deferred
* Conditional formatting on pivot data cells ❌ deferred

**Phase 11 — niche:**

* Bond pricing functions (PRICE, ODDFPRICE, ODDLYIELD)
* Custom table style export ⚠️ partial — toolkit round-trip via `customTableStyles` arg on `createExcelFile` ships, but the `TableStyleSelector` picker doesn't list user-authored styles and `TableView.styleName` isn't applied through `useSpreadsheetState` for them. UI wiring deferred
* Workbook named cell styles UI gallery ⚠️ partial — toolkit round-trip via `namedCellStyles` ships, but `CellStyleSelector`'s `userDefinedStyles` prop isn't auto-populated from imported named styles. UI wiring deferred
* Localized function names

**Phase 12 — linked data types:**

* Linked data type cells (Stocks, Geography, Wolfram) — partial: the `entity`-shaped `structuredValue` model + `dataTypeProviders` API already lets hosts wire custom providers (see `stockProvider` in the storybook), and `=A1.field` field-access syntax works. Round-tripping the OOXML `linkedDataType` element + Excel's built-in Stocks/Geography panel is deferred. Not blocked by anything fundamental.

## Explicitly out of scope

* Macros / VBA / ActiveX / OLE
* Power Query / Power Pivot / external data connections
* SmartArt graphics
* Cube functions (CUBEMEMBER, etc.)
* STOCKHISTORY (live market data)
* Custom XML parts
* 3D / surface charts (3D needs `echarts-gl` peer dep)
* Google Apps Script
* IMPORTRANGE / GOOGLETRANSLATE / GOOGLEFINANCE (Sheets-specific functions tied to Google services)

## Summary

The Rows n Columns Spreadsheet offers **high-fidelity** compatibility with Excel and Google Sheets across the features most apps use:

**Fully compatible:**

* All basic & advanced editing operations
* Full cell formatting palette (fonts, colors, borders, alignment, number formats, rotation)
* \~378 Excel-compatible functions including dynamic arrays + modern array helpers
* Named ranges — range-typed, static-value, formula-typed (`=MyVar` where MyVar is a formula), and named functions (LAMBDA-typed). Excel virtual-cell DAG model with scope-aware (workbook + sheet) resolution and order-independent batch registration
* Tables, AutoFilter (incl. color/icon), multi-column sort, data validation
* Conditional formatting (boolean, gradient, data bars, icon sets, custom formula)
* Charts (column, bar, line, area, pie/doughnut, scatter/bubble, radar, stock-line, treemap, sunburst)
* Embedded images, hyperlinks, sparklines, legacy comments
* Multi-sheet workbooks with full sheet management
* Row/column outline & grouping (XLSX + ODS round-trip)
* Slicers (table-backed)
* Themes (light / dark variants)
* **XLSX + ODS round-trip** for all of the above
* Password-encrypted XLSX export
* CSV import/export

**Built-in features Excel/Sheets don't have:**

* Real-time collaboration (YJS, ShareDB, Supabase) out of the box
* Web-worker formula engine
* Custom cell editors + renderers
* Multi-instance grids on one page
* OpenAI / ChatGPT magic fill integration
* Schema-based tables
* React-native customisation throughout

**Known gaps** (deferred — see Phase tables above):

* Threaded comments (legacy comments work)
* Recursive LAMBDA (rest of Phase 7 family + GROUPBY/PIVOTBY shipped)
* Pivot table **calculated fields/items** + pivot-backed slicer OLAP cache (rest of Phase 10 shipped — cache+def round-trip, authoring UI, GETPIVOTDATA, show-values-as, grouping, slicer wiring)
* 3D / OHLC / surface charts
* Macros, Power Query, Cube functions, linked data types (out of scope)

For most spreadsheet application use cases, this provides production-grade compatibility with Excel and Google Sheets — and adds collaboration + extensibility features that neither has natively.


# Roadmap

This document outlines the planned architectural improvements to evolve the spreadsheet engine into a high-performance, WebAssembly-driven calculation platform.

## Phase 1: Critical Reliability & Performance Fixes

*(Status: Planned)*

### 1. Fix Iterative Calculation Convergence

* **Problem**: Circular dependency checks currently return `POSITIVE_INFINITY` for non-numeric types (strings, booleans), causing infinite loops until `maxIterations` is hit.
* **Solution**: Implement strict equality checks (`===`) for non-numeric values in the convergence detection logic.
* **Impact**: Prevents wasted CPU cycles and unresponsive workers during circular reference evaluation involving status toggles or text.

### 2. Optimize Formula Parser Instantiation

* **Problem**: A new `FormulaParser` instance is created for *every single cell* during evaluation, causing massive memory churn and initialization overhead.
* **Solution**:
  * Implement a singleton `FormulaParser` in the worker.
  * Use a mutable `TaskContext` to swap out the `scopeMap` and `sheetId` for each evaluation pass without recreating the parser.
* **Impact**: Significant reduction in garbage collection pauses and faster batch processing.

### 3. Coalesce User Inputs

* **Problem**: Rapid user inputs (e.g., dragging a handle, fast typing) can flood the worker with calculation requests.
* **Solution**: Set a default `coalesceDelayMs` (e.g., 16ms) in `useCalculationWorker`.
* **Impact**: Smoother UI interactions by batching rapid-fire updates into single calculation frames.

## Phase 2: Architecture Evolution (WASM & Rust)

*(Status: Design Phase)*

### 1. In-Memory Cell Store (The "Source of Truth" Shift)

Move the authoritative state from React/JS to the Web Worker (Rust/WASM).

* **Current Architecture**:
  * React holds state → Serializes JSON → Worker calculates → Returns JSON.
  * Performance bottleneck: Data serialization and Main Thread blocking.
* **New Architecture**:
  * **Worker/WASM**: Acts as the In-Memory Database. Stores `Grid { cells: HashMap<Coord, CellData> }`.
  * **React**: Acts as the "View". Stores only the `ViewportCache` (visible cells) and `Y.Doc` (for sync).
  * **Flow**:
    1. React sends lightweight "Delta" (`A1 = 5`) to Worker.
    2. Worker updates internal WASM state.
    3. Worker recalculates affected dependencies in-memory (Zero-Copy).
    4. Worker sends back only the diffs (`B1 changed to 10`).
  * **Impact**: 60 FPS rendering regardless of dataset size (1M+ rows).

### 2. Rust-Based Formula Evaluation

* **Goal**: Replace `fast-formula-parser` (JS) with a Rust-based evaluation engine.
* **Why**: JS parsing is slow; Rust allows SIMD optimizations for array formulas and strictly typed math.
* **Plan**:
  * Expand `dag-wasm` to include an `EvaluationEngine`.
  * Port standard Excel functions to Rust.
  * Expose `evaluate(formula: &str)` directly to the DAG.

### 3. Yjs Synchronization Bridge

* **Goal**: Efficiently sync concurrent edits (CRDTs) with the linear memory of WASM.
* **Implementation**:
  * Use `Y.Doc` on the Main Thread for networking and conflict resolution.
  * Create a "Bridge" that observes `Y.Doc` changes and pushes binary/flat updates to the Worker.
  * Worker treats these updates as "External Inputs" and never writes back to Yjs directly (Unidirectional Data Flow).

## Phase 3: Excel Compatibility & Advanced Features

### 1. Precision & Date Systems

* **Date Systems**: Add support for both 1900 and 1904 date systems (Excel compatibility).
* **Number Precision**: Handle IEEE 754 floating-point quirks (e.g., `0.1 + 0.2`) to match Excel's 15-digit precision behavior.

### 2. "Smart" Range Recalculation

* **Problem**: Dependencies on large ranges (e.g., `SUM(A1:A10000)`) trigger massive data transfers.
* **Solution**: With the **In-Memory Cell Store**, range functions read directly from WASM memory. No data transfer is required between the calculation logic and the data store.

***

## Architecture Diagrams

### Data Flow (Target State)

```mermaid
graph TD
    User[User Input] --> React[React Main Thread]
    
    subgraph "Main Thread (View & Sync)"
        React -->|1. Optimistic Update| ViewCache[Viewport Cache (Zustand/useState)]
        ViewCache -->|Render| UI[Grid Component]
        
        React -->|2. Sync Changes| YDoc[Yjs Doc (CRDT)]
        YDoc -->|3. Observe & Broadcast| Bridge[Sync Bridge]
    end
    
    Bridge -->|4. Push Delta (Binary/JSON)| Worker[Web Worker]
    
    subgraph "Worker Thread (The Brain)"
        Worker -->|5. Update Memory| WasmStore[WASM Cell Store]
        
        subgraph "Rust/WASM Engine"
            WasmStore -->|Read| DAG[Dependency Graph]
            DAG -->|Topological Sort| Calc[Calculation Engine]
            Calc -->|Evaluate (In-Memory)| WasmStore
        end
        
        WasmStore -->|6. Extract Diff| DiffGen[Diff Generator]
    end
    
    DiffGen -->|7. Computed Updates (Visible Only)| React
    React -->|8. Merge Diffs| ViewCache
```


# Features


# Data validation

Validate and show errors to users if they enter invalid data

Validation rules can be added as part of your cellData. Below is an example of using `getDataValidation` with `CanvasGrid` to dynamically validate cells.

```tsx
import React, { useState } from "react";
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const MySpreadsheet = () => {
  const [dataValidations, onChangeDataValidations] = useState([
    {
      id: "validation1",
      condition: {
        type: "ONE_OF_LIST",
        values: [
          { userEnteredValue: "Singapore" },
          { userEnteredValue: "USA" },
          { userEnteredValue: "UK" },
        ],
      },
    },
  ]);

  const { getDataValidation } = useSpreadsheetState({
    dataValidations,
    onChangeDataValidations,
  });

  return (
    <CanvasGrid
      getCellData={(sheetId, rowIndex, columnIndex) => {
        if (rowIndex === 2 && columnIndex === 2) {
          return {
            dataValidation: "validation1", // Reference the validation ID
          };
        }
      }}
      getDataValidation={getDataValidation} // Pass the getDataValidation function to CanvasGrid
    />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```

The following validation types are supported:

```typescript
// Supported condition types
export const CONDITION_TYPES = [
  "NUMBER_GREATER",
  "NUMBER_GREATER_THAN_EQ",
  "NUMBER_LESS",
  "NUMBER_LESS_THAN_EQ",
  "NUMBER_EQ",
  "NUMBER_NOT_EQ",
  "NUMBER_BETWEEN",
  "NUMBER_NOT_BETWEEN",
  "TEXT_CONTAINS",
  "TEXT_NOT_CONTAINS",
  "TEXT_STARTS_WITH",
  "TEXT_ENDS_WITH",
  "TEXT_EQ",
  "TEXT_IS_EMAIL",
  "TEXT_IS_URL",
  "DATE_EQ",
  "DATE_BEFORE",
  "DATE_AFTER",
  "DATE_ON_OR_BEFORE",
  "DATE_ON_OR_AFTER",
  "DATE_BETWEEN",
  "DATE_NOT_BETWEEN",
  "DATE_IS_VALID",
  "ONE_OF_RANGE",
  "ONE_OF_LIST",
  "BLANK",
  "NOT_BLANK",
  "CUSTOM_FORMULA",
  "BOOLEAN",
  "TEXT_NOT_EQ",
  "DATE_NOT_EQ",
] as const;
```

{% content-ref url="/pages/j5ZkTo68wJt5vRgQDc2y" %}
[Data Validation Editor](/configuration/components/data-validation-editor)
{% endcontent-ref %}


# Custom formula evaluation

Formula parser and calculation is plug n play

Spreadsheet is completely headless, you can use your own formula parser and evaluator. You can choose to use client-side or server side evaluation.

## Formula parser

The default formula parser is based on open source `fast-formula-parser` . All formula calculations are done on the client-side.

## Custom functions

If you are using `useSpreadsheetState` hook to render the data of the Spreadsheet, its easy to add custom functions.

### 1. Create your named function

```typescript
import type FormulaParser from "@rowsncolumns/fast-formula-parser";
import type { FunctionArgument } from "@rowsncolumns/calculator";

const SAY_WORLD = (parser: FormulaParser, arg: FunctionArgument) => {
  if (arg.value === 'hello') {
    return 'world'
  }
}
```

### 2. Create a function description

Function description appears in the dropdown when users enters the formula

```json
const functionDescriptions = [{
  datatype: "Text",
  title: "SAY_WORLD",
  syntax: "SAY_WORLD(value)",
  description: "Returns world if user says hello.",
  example: "SAY_WORLD('hello')",
  usage: ["SAY_WORLD('oops')"],
  parameters: [
    {
      title: "value",
      description: "The text that user enters.",
    },
  ],
}]
```

### 3. Pass the function and description to `useSpreadsheetState`

```tsx
const MySpreadsheet = () => {
  const {} = useSpreadsheetState({
    functions: {
      SAY_WORLD
    }
  })
  
  return (
    <CanvasGrid
      functionDescriptions={functionDescriptions}
    />
  )
}
```

Your new custom formula should be ready to use

## Using your own formula evaluation

There are 2 ways to use a custom formula evaluation engine.

### 1. enqueueCalculation in useSpreadsheetState

```tsx
import type { CellCoordinate } from "@rowsncolumns/dag";
const MySpreadsheet = () => {
  const {} = useSpreadsheetState({
    enqueueCalculation: (type: 'add' | 'remove' | 'dirty', position: CellCoordinate) => {
      // Process calculations and update sheet
      const value = getCellValue (position)
      if (value.str(0) === '=') {
        const result = myCustomEvaluationQueue(value)
        // Call setState to update result
        onChangeSheetData()
      }  
    }
  })
  
  return (
    <CanvasGrid
    />
  )
}
```

If you are using a custom calculator, assuming that you are maintaining the cell dependency graph, you will also have to provide `getDependents` and `getPrecendents` API to `useSpreadsheetState`

### 2. Sending results to back-end

You can either use above method to send the calculation to the back-end to evaluate formulas or you can use the headless-ui, without `useSpreadsheetState` hook

```typescript
import type { CellCoordinate } from "@rowsncolumns/dag";
const MySpreadsheet = () => {
  
  return (
    <CanvasGrid
      onChange={(sheetId, rowIndex, columnIndex, value: string) => {
        if (value.str(0) === '=') {
          const result = await fetch("post", { value })
          onChangeSheetData({ result })
        }
      })
    />
  )
}
```

## Web Worker support

`@rowsncolumns/calculation-worker` package supports calculation via a web worker. The cell dependency graphs is still on the main UI thread, but formula parsing and calculation happens on a web worker


# Iterative calculation

Iterative calculation allows circular references to converge over repeated evaluations, similar to Excel. This is useful for finance models where the result depends on its own output.

## Enable it

```ts
const state = useSpreadsheetState({
  iterativeCalculation: {
    enabled: true,
    maxIterations: 100,
    maxChange: 0.001,
  },
});
```

Notes:

* Defaults match Excel (100 iterations, 0.001 max change).
* When disabled, circular references return `#REF!`.
* When enabled, non-converging formulas return `#NUM!`.

## Supported modes

* **Single-threaded:** `useCalculation` runs iterative loops on the UI thread.
* **Worker mode:** `useCalculationWorker` delegates iterative groups to the worker.

## Example formulas to test

Converging loop:

```
// A1
0
// A1 (replace)
=(A1+10)/2
```

Expected: A1 converges to \~10.

Non-converging loop:

```
// A2
0
// A2 (replace)
=A2+1
```

Expected: A2 returns `#NUM!`.

## Testing

Unit tests for single-threaded iterative calculation:

```sh
yarn workspace @rowsncolumns/spreadsheet-state test -- use-calculation.spec.ts
```

Worker iterative tests:

```sh
yarn workspace @rowsncolumns/calculation-worker test
```


# Formula auditing

Visualize formula dependencies with trace precedents and dependents

Formula auditing allows you to visualize and understand the relationships between cells in your spreadsheet. You can trace which cells feed data into a formula (precedents) and which cells use a specific cell in their formulas (dependents).

## Overview

The `useSpreadsheetState` hook provides three methods for auditing formula dependencies:

* `onTracePrecedents` - Shows arrows from cells that the selected cell depends on
* `onTraceDependents` - Shows arrows to cells that depend on the selected cell
* `onRemoveArrows` - Clears all trace arrows from the display

## Basic Usage

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const MySpreadsheet = () => {
  const {
    activeCell,
    activeSheetId,
    arrows,
    onTracePrecedents,
    onTraceDependents,
    onRemoveArrows,
    ...rest
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets,
    onChangeSheetData
  });

  return (
    <div>
      <div className="toolbar">
        <button onClick={() => onTracePrecedents(activeSheetId, activeCell)}>
          Trace Precedents
        </button>
        <button onClick={() => onTraceDependents(activeSheetId, activeCell)}>
          Trace Dependents
        </button>
        <button onClick={onRemoveArrows}>
          Remove Arrows
        </button>
      </div>
      <CanvasGrid
        {...rest}
        activeCell={activeCell}
        arrowComponents={arrows}
      />
    </div>
  );
};
```

## API Reference

### onTracePrecedents

Traces and displays arrows from cells that the selected cell depends on (precedents) to the active cell.

**Signature:**

```typescript
onTracePrecedents(sheetId: number, cell: CellInterface): void
```

**Parameters:**

* `sheetId: number` - The ID of the sheet containing the cell
* `cell: CellInterface` - Object with `rowIndex` and `columnIndex` properties

**Example:**

If cell C6 contains the formula `=SUM(A1:A5)`, calling `onTracePrecedents` on C6 will display arrows from the range A1:A5 pointing to C6, showing that C6 depends on those cells for its value.

```typescript
// Trace precedents for cell C6 (rowIndex: 5, columnIndex: 2)
onTracePrecedents(activeSheetId, { rowIndex: 5, columnIndex: 2 });
```

### onTraceDependents

Traces and displays arrows from the selected cell to all cells that depend on it (dependents).

**Signature:**

```typescript
onTraceDependents(sheetId: number, cell: CellInterface): void
```

**Parameters:**

* `sheetId: number` - The ID of the sheet containing the cell
* `cell: CellInterface` - Object with `rowIndex` and `columnIndex` properties

**Example:**

If cell A1 contains a value and is referenced by formulas in cells C6 and D10, calling `onTraceDependents` on A1 will display arrows from A1 pointing to C6 and D10.

```typescript
// Trace dependents for cell A1 (rowIndex: 0, columnIndex: 0)
onTraceDependents(activeSheetId, { rowIndex: 0, columnIndex: 0 });
```

### onRemoveArrows

Clears all trace arrows from the spreadsheet display.

**Signature:**

```typescript
onRemoveArrows(): void
```

**Parameters:** None

**Example:**

```typescript
// Clear all arrows
onRemoveArrows();
```

### arrows

The `arrows` property contains the rendered arrow components that visualize the cell dependencies. These must be passed to the `CanvasGrid` component via the `arrowComponents` prop.

**Type:**

```typescript
arrows: React.ReactNode[]
```

**Usage:**

```tsx
<CanvasGrid
  {...spreadsheetProps}
  arrowComponents={arrows}
/>
```

## Use Cases

### Debugging Complex Formulas

When working with complex spreadsheets containing many interrelated formulas, formula auditing helps you understand the data flow:

```typescript
// User clicks on a cell with a complex formula
const handleCellClick = (cell: CellInterface) => {
  // Show what feeds into this formula
  onTracePrecedents(activeSheetId, cell);
};
```

### Impact Analysis

Before changing a cell value, you can see which other cells will be affected:

```typescript
// User wants to change cell A1
const handleBeforeEdit = (cell: CellInterface) => {
  // Show all cells that depend on this cell
  onTraceDependents(activeSheetId, cell);

  // Show warning if there are many dependents
  const dependents = getDependents(cell);
  if (dependents.length > 10) {
    alert(`Warning: This change will affect ${dependents.length} cells`);
  }
};
```

### Interactive Formula Explorer

Create an interactive tool that lets users explore formula relationships:

```tsx
const FormulaExplorer = () => {
  const [mode, setMode] = useState<'precedents' | 'dependents' | null>(null);

  const handleCellSelect = (cell: CellInterface) => {
    onRemoveArrows();

    if (mode === 'precedents') {
      onTracePrecedents(activeSheetId, cell);
    } else if (mode === 'dependents') {
      onTraceDependents(activeSheetId, cell);
    }
  };

  return (
    <div>
      <div className="mode-selector">
        <button onClick={() => setMode('precedents')}>
          Trace Precedents Mode
        </button>
        <button onClick={() => setMode('dependents')}>
          Trace Dependents Mode
        </button>
        <button onClick={() => {
          setMode(null);
          onRemoveArrows();
        }}>
          Clear Mode
        </button>
      </div>
      <CanvasGrid
        onCellClick={handleCellSelect}
        arrowComponents={arrows}
        {...spreadsheetProps}
      />
    </div>
  );
};
```

## Visual Styling

The arrows are rendered with default styling, but they respond to cell positions and handle both single cells and ranges:

* **Single Cell References**: Arrows point from one cell to another
* **Range References**: A border is drawn around the range, with an arrow pointing from the range to the dependent cell
* **Color**: Default arrow color is `#000000` (black)

The arrow components are absolutely positioned and overlay the grid without interfering with cell interactions.

## Notes

* Arrows are automatically cleared when you call `onTracePrecedents` or `onTraceDependents` again
* Only call `onRemoveArrows()` when you want to clear all arrows completely
* The `arrows` are React components that must be rendered via the `arrowComponents` prop
* The tracing works with the built-in dependency graph maintained by the calculator
* Both single cell references and range references are supported


# Real-time data

Display real-time data, by subscribing to Websockets to REST API

{% hint style="info" %}
Learn more about [Custom formulas](/configuration/features/custom-formula-evaluation)
{% endhint %}

## Display static data from REST API

Formula functions are asynchronous by default. An example would be to get Crypto prices from `Gemini`

{% code title="cryptoprice.ts" %}

```typescript
import { FunctionArgument } from "@rowsncolumns/calculator";
import type FormulaParser from "@rowsncolumns/fast-formula-parser";
import FormulaError from "@rowsncolumns/fast-formula-parser/formulas/error";

// Usage: 
// =CRYPTOPRICE("btcusd")
export const CRYPTOPRICE = async (
  parser: FormulaParser,
  arg: FunctionArgument
) => {
  if (!arg || !arg.value) {
    throw new FormulaError("#VALUE!", "Symbol pair is required");
  }
  
  // Get data from GEMINI
  const fetchPrices = async () => {
    try {
      const results = await fetch(
        `https://api.gemini.com/v2/ticker/${String(arg.value).toLowerCase()}`,
        {
          method: "GET",
        }
      );
      const values = await results.json();
      return [[Number(values.ask), Number(values.bid)]];
    } catch (err) {}
  };  

  return await fetchPrices();
};

```

{% endcode %}

As crypto prices change every millisecond, we need to ability to update this data. But currently there is no way to do that, as formula functions are stateless.

To achieve this, you can use `calculationPipeline` hook.

## Subscribing to REST API

`calculationPipeline` hook contains a `callback` and it should return an `unsubscribe` function.

Above REST API can be written as below, so that we can poll the API every 5 seconds.

```typescript
import { FunctionArgument, calculationPipeline } from "@rowsncolumns/calculator";
import type FormulaParser from "@rowsncolumns/fast-formula-parser";
import FormulaError from "@rowsncolumns/fast-formula-parser/formulas/error";

export const CRYPTOPRICE = async (
  parser: FormulaParser,
  arg: FunctionArgument
) => {
  if (!arg || !arg.value) {
    throw new FormulaError("#VALUE!", "Symbol pair is required");
  }

  const fetchPrices = async () => {
    try {
      const results = await fetch(
        `https://api.gemini.com/v2/ticker/${String(arg.value).toLowerCase()}`,
        {
          method: "GET",
        }
      );
      const values = await results.json();
      return [[Number(values.ask), Number(values.bid)]];
    } catch (err) {}
  };

  
  // Execute in isolated environment
  calculationPipeline(parser, (onUpdate) => {
    const timeout = setInterval(async () => {
      const values = await fetchPrices();
      if (values !== undefined) {
        onUpdate(values);
      }
    }, 5000);
    
    // Cleanup function
    return () => {
      clearInterval(timeout);
    };
  });

  return await fetchPrices();
};
```

## Subscribing to Websocket

Using `calculationPipeline` hook, we can connect to Websocket and subscribe to streaming data.

{% hint style="info" %}
For performance reasons, use throttling to prevent unnecessary Spreadsheet update
{% endhint %}

<pre class="language-typescript"><code class="lang-typescript"><strong>export const CRYPTOPRICE = (parser: FormulaParser, arg: FunctionArgument) => {
</strong>  if (!arg || !arg.value) {
    throw new FormulaError("#VALUE!", "Websocket URL is required");
  }
  
  calculationPipeline(parser, (onUpdate) => {
    let websocket = new WebSocket(arg.url)
    websocket.addEventListener('message', (event) => {
      onUpdate(event.data)
    })
    
    // Unsubscriber
    return () => {
      websocket.close()
    };
  });
  
  return `Connecting to websocket`
}
</code></pre>

{% hint style="info" %}
Formula lifecycle or `calculationPipeline` hook is called when selections/cells are moved or deleted, or copy pasted to another location.
{% endhint %}

It is advisable to use `rxjs` subscription for websocket updates, so that you can subscribe and unsubscribe to a subject upon disconnect.

## Web workers

With `calculationPipeline` , you can choose to run your code in a web worker. Initialise a single web worker or multiple web workers (if user enters same formula, calculationPipeline will be invoked)

```typescript
// Initialize a worker when Spreadsheet is loaded
// You can also initialize this in calculationPipeline, but you wouldnt
// want to create 1 worker per formula
const cryptoWorker = new Worker('./worker.js')

export const CRYPTOPRICE = (parser: FormulaParser, arg: FunctionArgument) => {
  if (!arg || !arg.value) {
    throw new FormulaError("#VALUE!", "Websocket URL is required");
  }
  
  calculationPipeline(parser, (onUpdate) => {
    cryptoWorker.postMessage({
      type: 'subscribe',
      symbol: arg.value
    })
    
    cryptoWorker.onmessage = (event) => {
      if (event.symbol === arg.value) {
        onUpdate(event.data)
      }
    }
    
    return () => {
      cryptoWorker.postMessage({
        type: 'unsubscribe',
        symbol: arg.value
      })
    }
  });
  
  return `Connecting to websocket`
}
```

## Cancelling async formula evaluation when dependencies change

`AbortController` is used to cancel long running formula when dependencies change, and trigger a new formula evaluation

```tsx
import type FormulaParser from "@rowsncolumns/fast-formula-parser";

export const BACK_END_API = (parser: FormulaParser) => {    
    const controller = new AbortController();
    const signal = controller.signal;

    // Listen to abort signals
    // When cell dependencies change and this formula is still running
    // Abort signal is emitted for users to abort this request
    parser.position?.signal.addEventListener('abort', () => {
        // Abort the fetch request
        controller.abort()
    })
    
    
    const request = await fetch('/url', { signal })
    const data = await request.json()
    
    return [ data.value ]
}
```


# Cell editors

Switch between custom or default cell editor

CanvasGrid accepts `CellEditor` props so developers can inject their own custom editor when required.

{% code overflow="wrap" %}

```tsx
import React from "react";
import { CanvasGrid, SpreadsheetProvider, CellEditorProps, CellEditor as DefaultCellEditor } from "@rowsncolumns/spreadsheet";

const MySpreadsheet = () => {
  return (
    <CanvasGrid
      sheetId={1}
      rowCount={100}
      columnCount={100}
      CellEditor={(props: CellEditorProps) => {
        if (props.cell.rowIndex === 2) {
          return (
            <select
              style={{
                position: "absolute",
                left: 0,
                top: 0,
                width: props.position.width,
                transform: `translate(${props.position.x}px, ${props.position.y}px)`,
              }}
              onChange={(e) => {
                props.onSubmit?.(e.target.value, props.activeCell);
              }}
            >
              <option>Foo</option>
              <option>bar</option>
            </select>
          );
        }
        // Fallback to default cell editor
        return <DefaultCellEditor {...props} />
      }}
    />
  );
};

const App = () =>  (
  <SpreadsheetProvider>
    <App />
  </SpreadsheetProvider>
);
```

{% endcode %}

### Date/Calendar input

Any cell with `numberFormat.type` set to `DATE` will show a date picker while editing.

The date picker is only visible if user is editing the cell using a pointer device like mouse, pen, touch/stylus etc.

<figure><img src="/files/x6RYjkgprNT3qRZ0mcA5" alt=""><figcaption><p>Date Input component</p></figcaption></figure>

## Customizing Suggestions Dropdown

Use `SuggestionsDropdown` prop to inject a custom dropdown component

{% code overflow="wrap" %}

```tsx
<CanvasGrid
  SuggestionsDropdown={(props: SuggestionDropdownProps) => {
    return (
     <div className="absolute shadow-md bg-rnc-background rounded-md py-1 max-w-[400px] overflow-auto top-full mt-[2px] z-10 w-full flex flex-col">
      {props.items.map((item) => {
        return (
         <div
          onClick={() => {
           props.onSelect(item)
         }}>{item.title}</div>
        )
      })}
     </div>
    )
  }}
/>
```

{% endcode %}


# Cell renderer

Customise Cells to your liking

Content cells and header cells can be changed to custom React components. Spreadsheet uses `ReactKonva` internally to render canvas declaratively.

All [Konva](https://konvajs.org/) components can be used in the Spreadsheet. Read more about React konva here - <https://github.com/konvajs/react-konva>

> Cells are only renderer if they have content, for performance reasons. Only if `getCellData` returns some cell value or cell style, will it be rendered

## Content cells

```tsx
import React from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  CellProps,
} from "@rowsncolumns/spreadsheet";
import { Rect, Text } from "react-konva";

export default {
  title: "Spreadsheet",
  component: SheetGrid,
};

const CustomCell = ({ x, y, width, height }: CellProps) => {
  return (
    <>
      <Rect x={x} y={y} width={width} height={height} fill="green" />
      <Text
        x={x}
        y={y}
        width={width}
        height={height}
        text="green"
        verticalAlign="middle"
        align="center"
        fill="white"
      />
    </>
  );
};

const MySpreadsheet = () => {
  return (
    <CanvasGrid
      sheetId={1}
      rowCount={100}
      columnCount={100}
      Cell={CustomCell}
      getCellData={(sheetId, rowIndex, columnIndex) => {
        if (rowIndex === 1 && columnIndex === 2) {
          return {
            fv: "Hello world",
          };
        }
      }}
    />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```

## Header cell

Use `HeaderCell` prop to customise header cells.

You can also instead localise header cell values using `getRowHeaderText` and `getColumnHeaderText`

```tsx
const HeaderCell = (props: HeaderCellprops) => {
  return <Text {...props} />
} 

const MySpreadsheet = () => {
  <CanvasGrid
    sheetId={1}
    rowCount={100}
    columnCount={100}
    HeaderCell={HeaderCell}
    getCellData={(sheetId, rowIndex, columnIndex) => {
      if (rowIndex === 1 && columnIndex === 2) {
        return {
          fv: "Hello world",
        };
      }
    }}
    getRowHeaderText={(rowIndex: number) => `Header: ${rowIndex}`}
    getColumnHeaderText={(columnIndex: number) => `Header: ${columnIndex}`}
  />
}
```


# Structured Cell Renderer

Create your own custom schema on cell and render custom cells like Sparklines, document objects etc

The Structured Cell Renderer allows you to render custom visualizations and content within spreadsheet cells. Instead of displaying plain text, you can render rich content like sparklines, charts, or any custom component.

## Overview

Structured results are special cell values that contain both data and rendering instructions. The spreadsheet automatically detects these structured values and renders them using the `StructuredResultRenderer` component.

## Supported Structured Results

### Sparklines

Sparklines are small inline charts that fit within a cell, perfect for visualizing trends in data.

```typescript
import type { SparkLineResult } from "@rowsncolumns/common-types";

const sparklineValue: SparkLineResult = {
  kind: "sparkline",
  data: [1, 5, 3, 7, 4, 9, 6],
  formattedValue: "Trend", // Fallback text
};
```

The sparkline will automatically render as a mini chart within the cell.

### Custom Structured Results

You can create your own structured result types by extending the `StructuredResult` interface:

```typescript
import type { StructuredResult } from "@rowsncolumns/common-types";

type MyCustomResult = {
  kind: "custom";
  data: any;
  formattedValue: string;
} & StructuredResult;
```

## Using Structured Cells

### 1. Define Your Custom Cell Data Type

```typescript
import type { CellData, StructuredResult } from "@rowsncolumns/spreadsheet";

type MyStructuredResult = {
  kind: "sparkline" | "custom";
  data: any[];
  formattedValue: string;
} & StructuredResult;

type MyCustomCellData = CellData<MyStructuredResult>;
```

### 2. Create Structured Cell Values

```typescript
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const { onChangeBatch } = useSpreadsheetState({
  // ... other props
});

// Add a sparkline to a cell
onChangeBatch(
  sheetId,
  {
    startRowIndex: 1,
    endRowIndex: 1,
    startColumnIndex: 1,
    endColumnIndex: 1,
  },
  [{
    ev: {
      structuredValue: {
        kind: "sparkline",
        data: [1, 5, 3, 7, 4, 9, 6],
        formattedValue: "Trend",
      }
    }
  }]
);
```

### 3. Custom Renderer (Optional)

If you need custom rendering logic beyond sparklines, you can provide your own `StructuredResult` component to `CanvasGrid`:

```typescript
import { Text } from "react-konva";
import type { StructuredResultProps } from "@rowsncolumns/spreadsheet";

const MyStructuredRenderer = (props: StructuredResultProps) => {
  const { structuredValue, ...rest } = props;
  
  if (structuredValue?.kind === "custom") {
    return (
      <Text
        {...rest}
        text={`Custom: ${structuredValue.formattedValue}`}
        fill="blue"
      />
    );
  }
  
  // Fallback to default renderer
  return null;
};

// Use in CanvasGrid
<CanvasGrid
  StructuredResult={MyStructuredRenderer}
  // ... other props
/>
```

## Getting Structured Values

You can access structured cell values through the spreadsheet state:

```typescript
const { getUserEnteredExtendedValue, getEffectiveExtendedValue } = useSpreadsheetState({
  // ... props
});

// Get the user-entered structured value
const userValue = getUserEnteredExtendedValue(sheetId, rowIndex, columnIndex);
console.log(userValue?.structuredValue);

// Get the computed/formatted structured value
const effectiveValue = getEffectiveExtendedValue(sheetId, rowIndex, columnIndex);
console.log(effectiveValue?.structuredValue);
```

## Formulas and Structured Results

You can create formulas that return structured results:

```typescript
import type FormulaParser from "@rowsncolumns/fast-formula-parser";
import type { FunctionArgument } from "@rowsncolumns/calculator";

const SPARKLINE = (parser: FormulaParser, dataRange: FunctionArgument) => {
  // Extract values from the range
  const values = dataRange.value; // Array of cell values
  
  return {
    kind: "sparkline",
    data: values.map(Number),
    formattedValue: `Sparkline(${values.length} points)`,
  };
};

// Register the function
const functions = {
  ...defaultFunctions,
  SPARKLINE,
};

// Use in a cell
// =SPARKLINE(A1:A10)
```

## Complete Example

```typescript
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  type CellData,
  type StructuredResult,
} from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

type MyStructuredResult = {
  kind: "sparkline";
  data: number[];
  formattedValue: string;
} & StructuredResult;

type MyCustomCellData = CellData<MyStructuredResult>;

function SpreadsheetWithStructuredCells() {
  const [sheets, setSheets] = useState([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet 1" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<MyCustomCellData>>({
    1: [
      null,
      {
        values: [
          null,
          {
            ev: {
              structuredValue: {
                kind: "sparkline",
                data: [1, 5, 3, 7, 4, 9, 6, 8, 5],
                formattedValue: "Sales Trend",
              },
            },
          },
        ],
      },
    ],
  });

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    // ... other hook values
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

## Use Cases

### Visualizing Trends

Sparklines are perfect for showing trends in financial data, metrics, or time series:

```typescript
// Sales data with inline trend visualization
const salesData = {
  ev: {
    structuredValue: {
      kind: "sparkline",
      data: monthlySales, // [100, 120, 115, 140, ...]
      formattedValue: "$1,234 total",
    },
  },
};
```

### Custom Indicators

Create custom visual indicators for status, progress, or ratings:

```typescript
type StatusResult = {
  kind: "status";
  status: "good" | "warning" | "error";
  formattedValue: string;
} & StructuredResult;
```

## Performance Considerations

* Structured results are rendered using canvas (react-konva), providing excellent performance even with many cells
* Complex visualizations should be optimized to avoid rendering bottlenecks
* Consider using `formattedValue` as a text fallback for export and copy operations

## Limitations

* Structured results are visual-only and don't affect cell value calculations
* When copying cells with structured results, the `formattedValue` is used as text
* Excel export will show the `formattedValue` as plain text


# Theming

Themes are used to custom the colours and default fonts of the Spreadsheet. You can also have customise colours of charts, embeds using a theme

## Theme type

Themes have the following properties. You can inject a theme to SheetGrid component using the `theme` prop.

```typescript
export type SpreadsheetTheme = {
  name: string;
  primaryFontFamily: string;
  themeColors: Record<ThemeColors, string>;
};

export type ThemeColors =
  | "text"
  | "background"
  | "accent1"
  | "accent2"
  | "accent3"
  | "accent4"
  | "accent5"
  | "accent6"
  | "hyperlink";
```

## Dark/Light mode

Theme also automatically support light or dark mode if used with `useSpreadsheetTheme` hook.

<pre class="language-tsx" data-overflow="wrap"><code class="lang-tsx"><strong>import React, { useState } from "react"
</strong><strong>import { 
</strong>  CanvasGrid, 
  SpreadsheetProvider,
  SpreadsheetTheme, 
  useSpreadsheetTheme,
  ColorMode
} from "@rowsncolumns/spreadsheet"

const MySpreadsheet = () => {
  const [colorMode, onChangeColorMode] = ueState&#x3C;ColorMode>('dark');
  const {
    isDarkMode,
    // To support dark mode, customize colors of headers and cell text foreground and background color
    ...spreadsheetColors
  } = useSpreadsheetTheme({
    colorMode
  });
  
  return (
    &#x3C;>
      &#x3C;button onClick={() => onChangeColorMode('light')}>Switch color mode&#x3C;/button>
      &#x3C;CanvasGrid
        {...spreadsheetColors}
        theme={spreadsheetTheme}
      />
    &#x3C;/>
  )
}

const App = () => {
  &#x3C;SpreadsheetProvider>
    &#x3C;MySpreadsheet />
  &#x3C;/SpreadsheetProvider>
}
</code></pre>

<figure><img src="/files/4JvilG3jvS6u6Ck4vTFX" alt=""><figcaption><p>Dark mode</p></figcaption></figure>

### Customising dark and light modes using CSS Variables

Spreadsheet 2 uses Tailwind for CSS styling of DOM components. The following are the CSS variables support by Spreadsheet 2.

`.rnc-dark` class is used for dark mode.

```css
:root {
 --rnc-background: 0 0% 100%;
 --rnc-foreground: 222.2 47.4% 11.2%;

 --rnc-muted: 0 0% 90.9%;
 --rnc-muted-foreground: 215.4 16.3% 46.9%;

 --rnc-popover: 0 0% 100%;
 --rnc-popover-foreground: 222.2 47.4% 11.2%;

 --rnc-card: 0 0% 100%;
 --rnc-card-foreground: 222.2 47.4% 11.2%;

 --rnc-border: 0 0% 78.0%;
 --rnc-input: 0 0% 82%;

 --rnc-primary: 211 100%  43.2%;
 --rnc-primary-foreground: 210 40% 98%;

 --rnc-secondary: 210 40% 96.1%;
 --rnc-secondary-foreground: 222.2 47.4% 11.2%;

 --rnc-accent: 0 0% 90.9%;
 --rnc-accent-foreground: 222.2 47.4% 11.2%;

 --rnc-destructive: 0 100% 50%;
 --rnc-destructive-foreground: 210 40% 98%;

 --rnc-warning: 24 100% 46.5%;
 --rnc-warning-foreground: 210 40% 98%;

 --rnc-ring: 215 20.2% 65.1%;

 --rnc-radius: 0.5rem;

 /* Scrollbar */
 --rnc-scrollbar-border: 0 0% 85.8%;
 --rnc-scrollbar-background: 0 0% 97.3%;
 --rnc-scrollbar-thumb: 0 0% 78.0%;
 
}
 
.rnc-dark {
 --rnc-background: 224 71% 4%;
 --rnc-foreground: 213 31% 91%;

 --rnc-muted: 0 0% 17.9%;
 --rnc-muted-foreground: 215.4 16.3% 56.9%;

 --rnc-popover: 224 71% 4%;
 --rnc-popover-foreground: 215 20.2% 65.1%;

 --rnc-card: 224 71% 4%;
 --rnc-card-foreground: 213 31% 91%;

 --rnc-border: 0 0% 31.2%;
 --rnc-input: 216 34% 17%;

 --rnc-primary: 210 40% 98%;
 --rnc-primary-foreground: 222.2 47.4% 1.2%;

 --rnc-secondary: 222.2 47.4% 11.2%;
 --rnc-secondary-foreground: 210 40% 98%;

 --rnc-accent: 216 34% 17%;
 --rnc-accent-foreground: 210 40% 98%;

 --rnc-destructive: 0 100% 50%;
 --rnc-destructive-foreground: 210 40% 98%;

 --rnc-warning: 24 100% 58.5%;
 --rnc-warning-foreground: 210 40% 98%;

 --rnc-ring: 216 34% 17%;

 --rnc-radius: 0.5rem;

 /* Scrollbar */
 --rnc-scrollbar-border: 0 0% 24.3%;
 --rnc-scrollbar-background: 0 0% 11.0%;
 --rnc-scrollbar-thumb: 0 0% 31.2%;
}
```


# Styling

Add borders, colours, stroke styles or custom gradients very easily

CellData supports cell formatting via the `uf` field (short form of `userEnteredFormat`). New writes should set `uf`; the effective format used at render time is derived on the fly.

```typescript
type CellData = {
  ... // Omitted for brevity
  uf?: CellFormat | StyleReference;
};

export type CellFormat = {
  backgroundColor?: Color | string;
  borders?: Borders;
  textFormat?: TextFormat;
  numberFormat?: NumberFormat;
  horizontalAlignment?: HorizontalAlign;
  verticalAlignment?: VerticalAlign;
  wrapStrategy?: WrapStrategy;
  indent?: number;
  relativeIndent?: number;
  textRotation?: number | "vertical";
  shrinkToFit?: boolean;
  // Pattern fill (OOXML 18 patterns: solid, mediumGray, darkGrid,
  // lightTrellis, etc.). When set to a non-"none" value the renderer
  // tiles foregroundColor over backgroundColor.
  fillPattern?: FillPattern;
  foregroundColor?: Color | string;
  // OOXML <gradientFill>. When present the renderer paints the
  // gradient instead of the solid / pattern fill.
  gradient?: GradientFill;
};

export type WrapStrategy = "overflow" | "wrap" | "clip";

export type NumberFormat = {
  type: NumberFormatType;
  pattern?: string;
};

export type NumberFormatType =
  | "TEXT"
  | "NUMBER"
  | "PERCENT"
  | "CURRENCY"
  | "DATE"
  | "TIME"
  | "DATE_TIME"
  | "FRACTION"
  | "SCIENTIFIC"
  | "SPECIAL";

export type HorizontalAlign = "left" | "right" | "center";
export type VerticalAlign = "top" | "middle" | "bottom";

export type Borders = {
  top?: Border;
  right?: Border;
  bottom?: Border;
  left?: Border;
  diagonalUp?: Border;
  diagonalDown?: Border;
};

export type Border = {
  // All 13 OOXML border styles supported.
  style: BorderStyle;
  width: number;
  color?: Color | string | null;
};

export type BorderStyle =
  | "dotted"
  | "dashed"
  | "solid"
  | "thin"
  | "solid_medium"
  | "solid_thick"
  | "double"
  | "hair"
  | "mediumDashed"
  | "mediumDashDot"
  | "dashDot"
  | "dashDotDot"
  | "slantDashDot";

export type TextFormat = {
  color?: Color | string;
  fontFamily?: string;
  fontSize?: number;
  bold?: boolean;
  italic?: boolean;
  strikethrough?: boolean;
  underline?: boolean;
  vertAlign?: "superscript" | "subscript";
};

export type GradientFill = {
  type: "linear" | "path";
  degree?: number;
  left?: number;
  right?: number;
  top?: number;
  bottom?: number;
  stops: Array<{ position: number; color: string }>;
};
```

{% hint style="info" %}
**Effective format is no longer persisted on `CellData`.** The renderer derives it on the fly from `uf`, the cell's effective value, and (for formula cells) precedent formats via `useSheetProperties.getEffectiveFormat`. The legacy `effectiveFormat` / `ef` fields are accepted when loading older saved data but should not be written.
{% endhint %}


# Context menu

Customise Context Menu

You can use `ContextMenu` prop to inject a custom menu

```tsx
import React from "react";
import { 
  CanvasGrid,
  SpreadsheetProvider,
  ContextMenuProps 
} from "@rowsncolumns/spreadsheet";
import { DropdownMenuContent, DropdownMenuItem } from "@rowsncolumns/ui"

// Only radix context menu is supported
const ContextMenu = (props: ContextMenuProps) => {
  return (
    <DropdownMenuContent
      align="start"
      sideOffset={0}
      onFocusOutside={(event) => {
        event.preventDefault();
      }}
      onCloseAutoFocus={(event) => {
        event.preventDefault();
      }}
    >
      <DropdownMenuItem>Hello world</DropdownMenuItem>
    </DropdownMenuContent>
}

const MySpreadsheet = () => {
  return (
    <CanvasGrid
      sheetId={1}
      rowCount={100}
      columnCount={100}
      ContextMenu={ContextMenu}
    />
  );
};

const App = () =>  (
  <SpreadsheetProvider>
    <App />
  </SpreadsheetProvider>
);
```


# Localisation

Add text in any language in the Spreadsheet

CanvasGrid renders what you pass it as a `fv` (formatted value) of the `CellData` object.

{% hint style="info" %}
RTL layout is not currently supported
{% endhint %}

## Setting the Locale

If you are using `useSpreadsheetState` for state management of Spreadsheet, you can pass in a `locale` prop to configure date and number formatting for different regions.

Locale is auto-detected from `Intl` JavaScript object if not provided.

```tsx
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state"

const Spreadsheet = () => {
  const { } = useSpreadsheetState({
    locale: 'en-US'
  })
}

export const App = () => {
  <SpreadsheetProvider>
    <Spreadsheet />
  <SpreadsheetProvider>
}
```

## Switching Locale at Runtime

When users switch locales (e.g., from `de-DE` to `en-US`), all formatted values in the spreadsheet need to be regenerated to reflect the new locale's formatting rules.

The `onChangeLocale` function handles this automatically:

```tsx
import { useState } from "react";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const Spreadsheet = () => {
  const [locale, setLocale] = useState("en-US");

  const { onChangeLocale } = useSpreadsheetState({
    locale,
    // ... other props
  });

  const handleLocaleChange = (newLocale: string) => {
    // Update the locale state
    setLocale(newLocale);
    // Regenerate all formatted values with the new locale
    onChangeLocale(newLocale);
  };

  return (
    <>
      <select
        value={locale}
        onChange={(e) => handleLocaleChange(e.target.value)}
      >
        <option value="en-US">English (US)</option>
        <option value="de-DE">German (Germany)</option>
        <option value="fr-FR">French (France)</option>
        <option value="es-ES">Spanish (Spain)</option>
        <option value="pt-BR">Portuguese (Brazil)</option>
      </select>
      {/* ... spreadsheet components */}
    </>
  );
};
```

### What Changes When Locale Switches

| Aspect                  | Behavior                                         |
| ----------------------- | ------------------------------------------------ |
| **Decimal separator**   | Changes (e.g., `.` in en-US, `,` in de-DE)       |
| **Thousands separator** | Changes (e.g., `,` in en-US, `.` in de-DE)       |
| **Currency symbol**     | Preserved (e.g., `$` stays `$`, `€` stays `€`)   |
| **Currency position**   | Preserved (encoded in the number format pattern) |
| **Numeric values**      | Preserved (only display formatting changes)      |
| **Date formats**        | Re-formatted according to locale conventions     |

### Example: Number Formatting Across Locales

The same numeric value `1234567.89` displays differently based on locale:

| Locale  | Formatted Value |
| ------- | --------------- |
| `en-US` | `1,234,567.89`  |
| `de-DE` | `1.234.567,89`  |
| `fr-FR` | `1 234 567,89`  |
| `es-ES` | `1.234.567,89`  |
| `pt-BR` | `1.234.567,89`  |

### Currency Formatting

When switching locales, currency symbols are preserved. Only the number formatting changes:

```
// USD currency cell with value 1234.56
en-US: $1,234.56
de-DE: $1.234,56  // Same symbol, different separators
fr-FR: $1 234,56  // Same symbol, French formatting
```

This matches the behavior of Google Sheets and Microsoft Excel.

### Supported Locales

The spreadsheet supports any valid BCP 47 locale identifier. Common examples include:

**European:**

* `de-DE` (German - Germany)
* `fr-FR` (French - France)
* `es-ES` (Spanish - Spain)
* `it-IT` (Italian - Italy)
* `nl-NL` (Dutch - Netherlands)
* `pl-PL` (Polish - Poland)
* `pt-PT` (Portuguese - Portugal)
* `sv-SE` (Swedish - Sweden)
* `da-DK` (Danish - Denmark)
* `fi-FI` (Finnish - Finland)
* `nb-NO` (Norwegian - Norway)

**Americas:**

* `en-US` (English - United States)
* `en-CA` (English - Canada)
* `fr-CA` (French - Canada)
* `es-MX` (Spanish - Mexico)
* `es-AR` (Spanish - Argentina)
* `pt-BR` (Portuguese - Brazil)

**Asia-Pacific:**

* `ja-JP` (Japanese - Japan)
* `zh-CN` (Chinese - China)
* `ko-KR` (Korean - Korea)
* `hi-IN` (Hindi - India)
* `th-TH` (Thai - Thailand)

### Undo/Redo Support

Locale changes are recorded in the undo history. Users can undo a locale change to restore the previous formatted values.

{% hint style="info" %}
Locale switching only affects the display format (`fv`). The underlying numeric values (`ev.nv`) are always preserved.
{% endhint %}

## Localising the UI

All built-in UI text is localised via component overrides: copy the default component's source (or write your own), translate the strings, and pass it as a prop. `CanvasGrid` accepts overrides for its embedded UI:

| Prop                      | Default UI                                                    |
| ------------------------- | ------------------------------------------------------------- |
| `ContextMenu`             | Right-click context menu                                      |
| `CellEditor`              | Inline cell editor                                            |
| `FilterBox`               | Filter dropdown in table/filter headers                       |
| `SelectionTitleComponent` | Range title shown on named/table selections                   |
| `PasteMenu`               | "Paste formatting / Split text to columns" menu after a paste |
| `SuggestionAction`        | Agent suggestion approve/reject popover                       |
| `HiddenRowMarkers`        | "Show row" markers for hidden rows                            |
| `HiddenColumnMarkers`     | "Show column" markers for hidden columns                      |

Other components such as `SheetTabs`, `SheetSwitcher`, `Toolbar` and `FormulaBar` are standalone — replace them with your own translated copies.

```tsx
import {
  CanvasGrid,
  type PasteMenuProps,
  type HiddenRowMarkersProps,
} from "@rowsncolumns/spreadsheet";

const LocalisedPasteMenu = (props: PasteMenuProps) => {
  // Copy the default PasteMenu source and translate the strings,
  // e.g. "Split text to columns" -> "Text in Spalten aufteilen"
  return <MyTranslatedPasteMenu {...props} />;
};

const LocalisedHiddenRowMarkers = ({
  hiddenDimensions,
  expandedDimensions,
  onShowRow,
}: HiddenRowMarkersProps) => {
  // Render a marker per hidden row range with a translated label,
  // e.g. aria-label="Zeile einblenden"
  return <MyTranslatedRowMarkers />;
};

const MySpreadsheet = () => (
  <CanvasGrid
    sheetId={1}
    rowCount={100}
    columnCount={100}
    PasteMenu={LocalisedPasteMenu}
    SuggestionAction={LocalisedSuggestionAction}
    HiddenRowMarkers={LocalisedHiddenRowMarkers}
    HiddenColumnMarkers={LocalisedHiddenColumnMarkers}
  />
);
```


# Named ranges

Name ranges and use them in formulas

Named ranges let you give a friendly name to a cell range, scalar value, formula, or LAMBDA — and reference it from any formula. Spreadsheet supports four shapes that all interop with Excel:

| Shape             | Example                    | Use it as                                                  |
| ----------------- | -------------------------- | ---------------------------------------------------------- |
| **Range-typed**   | `MyRange = A1:B10`         | `=SUM(MyRange)`                                            |
| **Static value**  | `MyVal = 42`               | `=MyVal*2` — scalar returned directly                      |
| **Formula-typed** | `MyVar = =SUM(A1:A10)`     | `=MyVar` — virtual cell, recomputes when precedents change |
| **Named LAMBDA**  | `MyFunc = =LAMBDA(x, x*2)` | `=MyFunc(5)` returns 10                                    |

## Basic usage

Pass `namedRanges` to `useSpreadsheetState` + `CanvasGrid`. The hook returns CRUD callbacks the UI can wire to:

```tsx
import {
  SpreadsheetProvider,
  CanvasGrid,
  NamedRange,
} from "@rowsncolumns/spreadsheet";
import { useState } from "react";
import {
  useSpreadsheetState,
  NamedRangeEditor,
} from "@rowsncolumns/spreadsheet-state";

const MySpreadsheet = () => {
  const [namedRanges, onChangeNamedRanges] = useState<NamedRange[]>([]);
  const {
    onCreateNamedRange,
    onUpdateNamedRange,
    onDeleteNamedRange,
    onRequestDefineNamedRange,
    onRequestUpdateNamedRange,
  } = useSpreadsheetState({
    namedRanges,
    onChangeNamedRanges,
  });

  return (
    <>
      <CanvasGrid namedRanges={namedRanges} />
      <NamedRangeEditor
        namedRanges={namedRanges}
        onCreateNamedRange={onCreateNamedRange}
        onUpdateNamedRange={onUpdateNamedRange}
        onDeleteNamedRange={onDeleteNamedRange}
      />
    </>
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```

The `NamedRangeEditor` component handles all four shapes — the user picks "Text or Formula" in the editor, and the engine resolves the type at evaluation time.

## Scoping — workbook vs sheet

Each name is either workbook-scoped (visible everywhere) or sheet-scoped (visible only on that sheet). When a sheet-scoped name shadows a workbook-scoped one with the same display name, Excel's lookup rules apply: **sheet-scope wins first**, then falls back to workbook scope.

```ts
const namedRanges: NamedRange[] = [
  // Workbook-scoped — visible everywhere
  { namedRangeId: "1", name: "Tax", value: "=0.08" },
  // Sheet-scoped — only on sheetId 1; overrides Tax on that sheet
  { namedRangeId: "2", name: "Tax", value: "=0.20", sheetId: 1 },
];

// On sheet 1: `=Tax` → 0.20
// On any other sheet: `=Tax` → 0.08
```

## Formula-typed names — the virtual-cell model

When `value` is a formula (`=SUM(A1:A10)`, `=price * 1.08`, etc.), the name becomes a first-class node in the calculation DAG. Cell formulas that reference the name get a real DAG input edge to that node, so the unified topological sort recomputes them whenever the named range's value changes.

```ts
const namedRanges: NamedRange[] = [
  { namedRangeId: "1", name: "price", value: "=A1" },
  { namedRangeId: "2", name: "fullPrice", value: "=price * 1.08" },
];

// Cell B1: =fullPrice
// When A1 changes → price recomputes → fullPrice recomputes → B1 recomputes
```

Order of registration doesn't matter — the engine does a two-pass batch register so `fullPrice` can reference `price` even if it's listed first.

## Named LAMBDAs — callable names

Set `value` to a LAMBDA expression and the engine treats the name as a callable function:

```ts
const namedRanges: NamedRange[] = [
  {
    namedRangeId: "1",
    name: "Commission",
    value: "=LAMBDA(sales, IF(sales > 1000, sales*0.1, sales*0.05))",
  },
];

// =Commission(500)  → 25
// =Commission(5000) → 500
```

Three resolution shapes are accepted on `onVariable`: a pre-built lambda value, a `{ref, value}` wrapper, or the raw `"=LAMBDA(...)"` source string (parsed + applied at call time). The fast-formula-parser engine handles all three identically.

## JSON round-trip

The DAG including all name → dependent edges is serializable. `Dag.toJSON` and `Dag.fromJSON` round-trip both directions of the cell ↔ named-range edges, so a rehydrated calculator matches the in-memory model exactly.


# Basic filter or Excel AutoFilter

Use a Basic filter or Autofilter to quickly find, filter and sort tabular data

To create a filter from your data,

1. Select a selection or focus on a cell which has some data around it
2. Press `Ctrl/Cmd + Shift + F`
3. Or Right click on the cell and select `Create a filter`

This will convert a selection to a filter

<figure><img src="/files/Nl5j8Yu4SOBPQIdKnSFU" alt=""><figcaption><p>Auto filter</p></figcaption></figure>

Wiring up autofilter using `useSpreadsheetState`

{% code overflow="wrap" %}

```tsx
import {
  SpreadsheetProvider,
  CanvasGrid,
  Sheet
} from "@rowsncolumns/spreadsheet";
import { useState } from "react";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const MySpreadsheet = () => {
  const [ sheets, onChangeSheets ] = useState<Sheet[]>([])
  const { basicFilter, onCreateBasicFilter } = useSpreadsheetState({
    sheets,
  });
  return (
    <CanvasGrid
      basicFilter={basicFilter}
      onCreateBasicFilter={onCreateBasicFilter}
    />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```

{% endcode %}


# Charts

Rowsncolumns Spreadsheet renders charts via ECharts (or Plotly via opt-in), parses + emits OOXML chart XML, and accepts a custom renderer if neither default fits.

Spreadsheet ships a rich chart pipeline with full XLSX + ODS round-trip on every supported chart type. The render backend is ECharts by default; Plotly is available as an opt-in (`@rowsncolumns/charts/plotly/basic-chart`).

## Supported chart types

All round-trip via XLSX import + export unless noted.

| Type                         | Notes                                                                                                                                 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Column (clustered / stacked) |                                                                                                                                       |
| Bar (clustered / stacked)    |                                                                                                                                       |
| Line (smooth / stepped)      |                                                                                                                                       |
| Area                         |                                                                                                                                       |
| Pie / doughnut               |                                                                                                                                       |
| Scatter                      |                                                                                                                                       |
| Bubble                       | Size dimension on top of scatter                                                                                                      |
| Radar                        |                                                                                                                                       |
| Treemap                      |                                                                                                                                       |
| Sunburst                     |                                                                                                                                       |
| Stock (line-derived)         | OHLC / candlestick variants deferred                                                                                                  |
| Waterfall                    | Emitted as `c15:waterfallChart` in `mc:Choice` + `c:barChart` fallback — Excel 2016+ renders natively, older Excel falls back to bars |
| Funnel                       | Same `c15:funnelChart` / fallback shape                                                                                               |
| Histogram                    | Auto/Sturges binning + bin-count override                                                                                             |
| Box & whisker                | quartileMethod, showMean, showOutlier                                                                                                 |
| Combo (mixed series)         | Per-series `seriesChartType` picker                                                                                                   |
| Dual-axis                    | `secondaryValueAxisOptions` round-trips                                                                                               |

Not yet supported: 3D variants (bar3D / column3D / pie3D / line3D / area3D), surface, OHLC / candlestick stock. These need `echarts-gl` and are deferred.

## Chart features

Every chart supports:

* **Titles, legends, axis labels** — formula-driven titles & labels (range references resolved at render)
* **Data labels** — value / category / percent
* **Trendlines** — linear, polynomial, exponential, log
* **Error bars** — value / percentage / stdev / custom
* **Data label styles** — number formats applied per series
* **Series colors** — per series + theme accents
* **Series gradient fills** — linear (with `degree`) and path (with `left`/`right`/`top`/`bottom` insets). Mirrors OOXML `<a:gradFill>` inside `<c:spPr>`. Set `series[].gradient` on the chart spec.
* **Negative-value coloring** — `invertIfNegative` + `negativeColor` on bar/column series
* **Chart move + resize** with anchor cell + pixel offsets
* **Chart editing UI** — `ChartEditorDialog` + `ChartEditor` components

## Installation

```bash
npm install @rowsncolumns/charts
```

ECharts is a peer dependency for the default backend:

```bash
npm install echarts echarts-for-react
```

For Plotly:

```bash
npm install @rowsncolumns/charts plotly.js react-plotly.js
```

## Basic usage

Wire charts through the `useCharts` hook (`@rowsncolumns/charts`) and pass the resulting handlers to `CanvasGrid`:

```tsx
import { useCharts, ChartEditorDialog, ChartEditor } from "@rowsncolumns/charts";

const App = () => {
  const [charts, onChangeCharts] = useState<EmbeddedChart[]>([]);

  const {
    onRequestEditChart,
    onDeleteChart,
    onMoveChart,
    onResizeChart,
    onUpdateChart,
    onCreateChart,
    selectedChart,
  } = useCharts({
    createHistory,
    onChangeCharts,
    getFormattedValue,
    getEffectiveValue,
  });

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        charts={charts}
        onMoveChart={onMoveChart}
        onResizeChart={onResizeChart}
        onDeleteChart={onDeleteChart}
        onRequestEditChart={onRequestEditChart}
      />
      <ChartEditorDialog>
        <ChartEditor
          chart={selectedChart}
          onUpdate={onUpdateChart}
        />
      </ChartEditorDialog>
    </SpreadsheetProvider>
  );
};
```

## Series gradient fills

Set `gradient` on any series — overrides `color`:

```ts
const chart: EmbeddedChart = {
  chartId: "c1",
  position: { /* … */ },
  spec: {
    chartType: "column",
    title: "Q1 Sales",
    domains: [{ sources: [/* category range */] }],
    series: [
      {
        dataLabel: "Revenue",
        gradient: {
          type: "linear",
          degree: 90,
          stops: [
            { position: 0, color: "#fde68a" },
            { position: 1, color: "#dc2626" },
          ],
        },
        sources: [/* value range */],
      },
    ],
  },
};
```

For radial fills use `type: "path"` with `left` / `right` / `top` / `bottom` insets (0..1 each):

```ts
gradient: {
  type: "path",
  left: 0.5,
  right: 0.5,
  top: 0.5,
  bottom: 0.5,
  stops: [
    { position: 0, color: "#ffffff" },
    { position: 1, color: "#7c3aed" },
  ],
}
```

ODS export emits the equivalent `<draw:gradient>` definition; XLSX export emits `<a:gradFill>` inside `<c:spPr>`.

## Bringing your own renderer

The default ECharts component is bundled, but charts are agnostic of the rendering layer. Pass your own component via the chart prop pipeline — the spec data flow is independent.

For an alternative renderer, import a different chart component (e.g. `import "@rowsncolumns/charts/plotly/basic-chart"`) — the spec format is identical.


# Slicers

Add, move, resize, and manage slicers for table filtering

Slicers let users filter table data using an interactive floating control on the sheet.

## Basic setup

Use `slicers` state + `onChangeSlicers` with `useSpreadsheetState`, then pass slicer props and handlers to `CanvasGrid`.

```tsx
import { useState } from "react";
import {
  CanvasGrid,
  Slicer,
  SlicerComponent,
  SlicerComponentProps,
} from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const Example = () => {
  const [slicers, onChangeSlicers] = useState<Slicer[]>([
    {
      slicerId: "slicer-1",
      position: {
        sheetId: 1,
        overlayPosition: {
          anchorCell: { rowIndex: 2, columnIndex: 12 },
          widthPixels: 150,
          heightPixels: 200,
          offsetXPixels: 10,
          offsetYPixels: 10,
        },
      },
      spec: {
        type: "table",
        tableIds: [1],
        columnIndex: 1,
      },
    },
  ]);

  const {
    onMoveSlicer,
    onResizeSlicer,
    onDeleteSlicer,
    onUpdateSlicer,
    onCreateSlicer,
    onRequestEditSlicer,
  } = useSpreadsheetState({
    slicers,
    onChangeSlicers,
  });

  return (
    <CanvasGrid
      slicers={slicers}
      onMoveSlicer={onMoveSlicer}
      onResizeSlicer={onResizeSlicer}
      onDeleteSlicer={onDeleteSlicer}
      onRequestEditSlicer={onRequestEditSlicer}
      getSlicerComponent={(props: SlicerComponentProps) => {
        return <SlicerComponent {...props} />;
      }}
    />
  );
};
```

## Programmatically create a slicer

You can create slicers using `onCreateSlicer`.

```tsx
onCreateSlicer({
  slicerId: "slicer-2",
  position: {
    sheetId: 1,
    overlayPosition: {
      anchorCell: { rowIndex: 5, columnIndex: 12 },
      widthPixels: 160,
      heightPixels: 220,
      offsetXPixels: 0,
      offsetYPixels: 0,
    },
  },
  spec: {
    type: "table",
    tableIds: [1],
    columnIndex: 2,
  },
});
```

## Available slicer handlers from `useSpreadsheetState`

* `onCreateSlicer`
* `onUpdateSlicer`
* `onDeleteSlicer`
* `onMoveSlicer`
* `onResizeSlicer`
* `onRequestEditSlicer`

All handlers integrate with undo/redo history when `onChangeSlicers` is provided.

## Notes

* Two slicer types ship: **table-backed** (`spec.type = "table"`, connects to one or more `TableView`s by id) and **pivot-backed** (`spec.type = "pivot"`, connects to one or more pivot tables by id + a field name).
* Pivot-backed slicers read distinct values from the bound pivot's source range (column matching `fieldName`). Selection routes through the new `onFilterPivot` callback — wire it to `usePivot.applySlicerSelectionToPivots`:

  ```tsx
  const { applySlicerSelectionToPivots } = usePivot({ /* … */ });

  return (
    <CanvasGrid
      slicers={slicers}
      onFilterPivot={applySlicerSelectionToPivots}
      pivotTables={pivotTables}
      /* … */
    />
  );
  ```
* For custom rendering, provide your own component through `getSlicerComponent`.
* XLSX round-trip: **table-backed slicers** ship in full; **pivot-backed** OLAP cube cache export is deferred (the slicer UI works on import + during a session, but a saved-and-reopened file may need to refresh from source).


# Embedded content

Embed external content, pictures and drawings

```tsx
<CanvasGrid
  embeds={[
    {
      embedId: 1,
      position: {
        sheetId: 1,
        overlayPosition: {
          anchorCell: { rowIndex: 2, columnIndex: 3},
          widthPixels: 500,
          heightPixels: 500,
          offsetXPixels: 0,
          offsetYPixels: 0,
        },
      },
      borderColor: "transparent",
      lockAspectRatio: true,
      spec: {
        type: "image",
        imageUrl: "https://www.google.com/images/srpr/logo3w.png",
      },
    }
  ]}
  getEmbedComponent={(props: EmbedComponentProps) => {
    return (
      <div>My Embed component</div>
    )
  }}
/>
```

{% hint style="info" %}
Currently, there is no UI to add/edit embeds
{% endhint %}

## Drag and Drop images

If you press and hold the `Shift` key and drag images on to the spreadsheet, they will be added on top of the sheet.


# Calculate on-demand

Trigger a full recalc whenever you need it.

If you are uploading an excel file or replacing the state of the spreadsheet, you will have to manually trigger a full recalc.

You can also disable formula evaluation or reset teh cell dependency graph using options.

To do so, `useSpreadsheetState` exposes `calculateNow` function.

{% code overflow="wrap" %}

```tsx
import {
  SpreadsheetProvider,
  CanvasGrid,
} from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const MySpreadsheet = () => {
  const { calculateNow } = useSpreadsheetState({
    ...
  });
  return (
    <div>
      <input type="file" onChange={() => {
        // Update sheets
        // update sheet Data
        // Update table
        calculateNow({
          // Formulas are not evaluated, only cell dependency graph is rebuilt
          disableEvaluation: true,
          // Optionally reset the entire dependency graph
          resetDependencyGraph: true,
        })
      })
      <CanvasGrid />
    </div>
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```

{% endcode %}


# Pivoting and Grouping

Create pivot tables with row/column grouping, aggregations, sorting, filtering, totals, drilldown, and slicer integration

Rowsncolumns Spreadsheet ships a full pivot-table authoring experience powered by DuckDB-WASM. Pivots run the same OOXML `pivotCacheDefinition` / `pivotTable` round-trip Excel uses, render through the existing CanvasGrid, and accept slicer-driven filtering on top of the regular sort + label-filter pipeline.

## Features at a glance

* **Row / column / value fields** with drag-to-reorder and per-field hide
* **Subtotals + grand totals** toggles (per-pivot, separate row + column)
* **Aggregation functions**: sum, count, avg, min, max, var, stddev, median, product
* **Show Values As** — `% of Grand Total`, `% of Row Total`, `% of Column Total`, `Running Total`, `Rank ↑` / `Rank ↓`
* **Field grouping** — date (year / quarter / month / week / day) or numeric (bucket size + optional offset)
* **Sort** — tri-state ↑ / ↓ / none on every field, including value-field sort
* **Label filter** — per-field popover with searchable checkbox list
* **Top N filter** — Top / Bottom × N by any value field
* **Refresh button** + auto-refresh on source mutation
* **Drilldown** — double-click any value cell to see the underlying source rows
* **Slicer ↔ pivot wiring** — slicer selection applies as a label filter to bound pivots
* **GETPIVOTDATA()** — formula-level lookup into a rendered pivot
* **XLSX round-trip** — full `pivotCacheDefinition` + `pivotTable` emission with `refreshOnLoad="1"`

## Installation

```bash
npm install @rowsncolumns/pivot
```

DuckDB-WASM is bundled as a dependency.

## Basic setup

`usePivot` returns the full callback surface; pass the same callbacks to `CanvasGrid` and to the `PivotEditor` component:

```tsx
import {
  usePivot,
  PivotEditor,
  NewPivotTableDialog,
  NewPivotTableEditor,
  createPivotTableFormats,
} from "@rowsncolumns/pivot";

const SpreadsheetWithPivot = () => {
  const [pivotTables, onChangePivotTables] = useState<PivotTable[]>([]);

  const {
    activeCell,
    activeSheetId,
    sheets,
    sheetData,
    onChangeSheets,
    onChangeSheetData,
    onChangeBatch,
    getEffectiveValue,
    onCreateNewSheet,
    createHistory,
    enqueueCalculation,
    onChangeActiveCell,
    cellXfsRegistry,
    sharedStringRegistry,
  } = useSpreadsheetState({
    onChangePivotTables,
    /* ... */
  });

  const getGridValues = useCallback(
    (range: SheetRange) => {
      const headers: any[] = [];
      const rows: any[][] = [];
      for (let r = range.startRowIndex; r <= range.endRowIndex; r++) {
        const row = [];
        for (let c = range.startColumnIndex; c <= range.endColumnIndex; c++) {
          row.push(getEffectiveValue(range.sheetId, r, c) ?? "");
        }
        if (r === range.startRowIndex) headers.push(...row);
        else rows.push(row);
      }
      return { headers, rows };
    },
    [getEffectiveValue],
  );

  const {
    // Create / delete / edit
    onCreatePivotTable,
    onRequestCreatePivotTable,
    onRequestEditPivotTable,
    onRequestDeletePivotTable,
    activePivotId,
    onClosePivotSettings,

    // Field management
    addRowPivot,
    addColumnPivot,
    addValue,
    removePivotField,
    changeFieldOrder,
    setAggregationFunction,

    // Source range
    updatePivotSourceRange,

    // Totals toggles
    toggleSubtotals,
    toggleRowGrandTotals,
    toggleColumnGrandTotals,

    // Sort + filter
    sortPivotField,
    removeSortPivotField,
    addFilter,
    removeFilter,

    // Show Values As + grouping
    setShowAs,
    setFieldGrouping,

    // Refresh + drilldown
    refreshPivot,
    executePivot,
    onDrillDownAtCell,

    // Slicer integration
    applySlicerSelectionToPivots,

    // Transforms
    transformPivotResults,
    transformChildRows,
    expandRowPivot,
  } = usePivot({
    locale: "en-US",
    pivotTables,
    activeCell,
    sheetId: activeSheetId,
    cellXfsRegistry,
    sharedStringRegistry,
    onChangeSheets,
    onChangePivotTables,
    onChangeSheetData,
    onCreateNewSheet,
    createHistory,
    enqueueCalculation,
    onChangeActiveCell,
    getGridValues,
  });

  const activePivotTable = pivotTables.find((t) => t.pivotId === activePivotId);

  return (
    <>
      <CanvasGrid
        pivotTables={pivotTables}
        onRequestEditPivotTable={onRequestEditPivotTable}
        onRequestDeletePivotTable={onRequestDeletePivotTable}
        onFilterPivot={applySlicerSelectionToPivots}
        /* ... */
      />

      {/* Pivot editor sidebar */}
      {activePivotTable && (
        <PivotEditor
          pivotTable={activePivotTable}
          updatePivotSourceRange={updatePivotSourceRange}
          addRowPivot={addRowPivot}
          addColumnPivot={addColumnPivot}
          addValue={addValue}
          removePivotField={removePivotField}
          changeFieldOrder={changeFieldOrder}
          setAggregationFunction={setAggregationFunction}
          toggleSubtotals={toggleSubtotals}
          toggleRowGrandTotals={toggleRowGrandTotals}
          toggleColumnGrandTotals={toggleColumnGrandTotals}
          sortPivotField={sortPivotField}
          removeSortPivotField={removeSortPivotField}
          addFilter={addFilter}
          removeFilter={removeFilter}
          setShowAs={setShowAs}
          setFieldGrouping={setFieldGrouping}
          refreshPivot={refreshPivot}
          onRequestClose={onClosePivotSettings}
        />
      )}

      <NewPivotTableDialog>
        <NewPivotTableEditor onSubmit={onCreatePivotTable} />
      </NewPivotTableDialog>
    </>
  );
};
```

## Authoring a pivot

### Programmatic creation

```ts
await onCreatePivotTable({
  pivotId: uuid(),
  source: {
    sheetId: 1,
    startRowIndex: 1,
    endRowIndex: 100,
    startColumnIndex: 1,
    endColumnIndex: 6,
  },
  targetPosition: { rowIndex: 1, columnIndex: 8, sheetId: 1 },
  rows: [{ field: "Region" }, { field: "Salesperson" }],
  columns: [{ field: "Quarter" }],
  values: [
    { field: "Revenue", aggFunc: "sum" },
    { field: "Units", aggFunc: "sum" },
  ],
});
```

### Right-click → "Create pivot table"

The `NewPivotTableDialog` is the in-app entry. Wire `onRequestCreatePivotTable` into the right-click context menu and the user picks the source range + target location.

## Show Values As

Switch a value field's display mode without changing the underlying aggregate. The transform runs in JS after DuckDB returns the raw `sum(...)` so any aggregator works.

```ts
// Six modes ship:
await setShowAs(pivotId, "Revenue", "as_is");          // raw aggregate (default)
await setShowAs(pivotId, "Revenue", "pct_of_total");   // PERCENT format auto-applied
await setShowAs(pivotId, "Revenue", "pct_of_row");
await setShowAs(pivotId, "Revenue", "pct_of_column");
await setShowAs(pivotId, "Revenue", "running_total");
await setShowAs(pivotId, "Revenue", "rank_desc");      // 1 = highest
await setShowAs(pivotId, "Revenue", "rank_asc");       // 1 = lowest
```

PERCENT (`0.00%`) and NUMBER (`0`) cell formats are auto-derived from the chosen mode and applied to the value + total cells.

## Field grouping

Group a date field by period or a numeric field by bucket size. Implemented as a DuckDB `EXCLUDE`-based SOURCE CTE rewrite — all downstream pivot logic sees the grouped value transparently.

```ts
// Group OrderDate column by month
await setFieldGrouping(pivotId, "OrderDate", {
  type: "date",
  period: "month",  // year | quarter | month | week | day
});

// Group Amount column into $1000 buckets starting at $0
await setFieldGrouping(pivotId, "Amount", {
  type: "numeric",
  bucketSize: 1000,
  startAt: 0,
});

// Clear the grouping (revert to raw column)
await setFieldGrouping(pivotId, "OrderDate", null);
```

## Sort

Tri-state ↑/↓/none. Sort on any row/column field by its labels, or on any value field by its aggregate.

```ts
// Sort regions A→Z
await sortPivotField(pivotId, "Region", "asc");
// Sort by total revenue (largest first)
await sortPivotField(pivotId, "Revenue", "desc");
// Clear the sort on a field
await removeSortPivotField(pivotId, "Region");
```

Value-field sort works in both layouts: grouping-only (single value column) AND column-pivoted (multiple `colVal_agg(field)` columns) — the matcher accepts both alias shapes.

## Label filter + Top N

Per-field set filter:

```ts
await addFilter(pivotId, "Region", {
  filterType: "set",
  values: ["North", "East"],
});

// Clear it
await removeFilter(pivotId, "Region");
```

Top N filter (the editor's "Top N filter" toggle does this for you):

```ts
await addFilter(pivotId, "Region", {
  filterType: "topN",
  direction: "top",      // "top" | "bottom"
  count: 10,
  byField: "Revenue",    // any value field
});
```

The filter is emitted as a subquery against the SOURCE CTE:

```sql
"Region" IN (SELECT "Region" FROM SOURCE GROUP BY "Region" ORDER BY sum("Revenue") DESC LIMIT 10)
```

## Totals toggles

```ts
await toggleSubtotals(pivotId, false);          // Hide subtotals
await toggleRowGrandTotals(pivotId, false);     // Hide bottom Grand Total row
await toggleColumnGrandTotals(pivotId, false);  // Hide right Grand Total column
```

Default is `true` for all three (Excel default).

## Refresh + auto-refresh

```ts
await refreshPivot(pivotId);  // Manual — re-run against current source data
```

Auto-refresh fires when `sourceDataVersion` bumps — typically when a cell in the pivot's source range changes. The Refresh icon in `PivotEditor` is a convenience for the manual case.

## Drilldown

Double-click any value cell to see the underlying source rows. Wire `onDrillDownAtCell` to your own modal / sheet / panel:

```ts
const handleDrilldown = async (cell: { rowIndex: number; columnIndex: number }) => {
  const result = await onDrillDownAtCell(pivotId, cell);
  // result.rows is an array of source rows that contributed to that cell
  openDrilldownModal(result);
};
```

## Slicer integration

Wire the pivot slicer's `onFilterPivot` callback directly to `applySlicerSelectionToPivots`:

```tsx
<CanvasGrid
  onFilterPivot={applySlicerSelectionToPivots}
  /* ... */
/>
```

Slicer selection then routes through the pivot's filter pipeline — checking/unchecking values applies a `filterType: "set"` filter on the pivot's `fieldName`. Empty selection clears the filter (shows all values).

## GETPIVOTDATA()

Look up a value cell in a rendered pivot from any formula:

```
=GETPIVOTDATA("Revenue", $H$1, "Region", "North", "Quarter", "Q3")
```

The function scans the pivot's row + column header bands for the supplied `(field=item)` tuples and returns the matching cell. See [Formula evaluation](/getting-started/formula-evaluation) for syntax details.

## XLSX round-trip

Pivot tables export to a full `xl/pivotCache/pivotCacheDefinitionN.xml` + `xl/pivotTables/pivotTableN.xml` pair with `refreshOnLoad="1"` so Excel rebuilds the cache from the live source on open. Positions, row/column/value fields, aggregation function, sort state, and filter state all round-trip.

Calculated fields / calculated items and the pivot-backed slicer OLAP cube cache export are still deferred.

## PivotTable type

```ts
type PivotTable = {
  pivotId: string | number;
  source?: SheetRange;
  targetPosition: SheetCoordinate;
  pivotRange?: SheetRange;  // current rendered range; the engine sets this
  rows: PivotGroup[];
  columns: PivotGroup[];
  values: PivotValue[];
  filters?: Record<string, any>;
  sortModel?: Array<{ field: string; sort: "asc" | "desc" }>;
  // Totals toggles — default true if omitted
  showSubtotals?: boolean;
  showRowGrandTotals?: boolean;
  showColumnGrandTotals?: boolean;
};

type PivotGroup = {
  field: string;
  displayName?: string;
  sortOrder?: SortOrder;
  grouping?:
    | { type: "date"; period: "year" | "quarter" | "month" | "week" | "day" }
    | { type: "numeric"; bucketSize: number; startAt?: number };
};

type PivotValue = {
  field: string;
  displayName?: string;
  aggFunc: "sum" | "count" | "avg" | "min" | "max" | "var" | "stddev" | "median" | "product";
  showAs?:
    | "as_is"
    | "pct_of_total"
    | "pct_of_row"
    | "pct_of_column"
    | "running_total"
    | "rank_desc"
    | "rank_asc";
};
```

## Performance notes

* DuckDB-WASM runs the PIVOT operator entirely in the browser. Datasets up to a few million rows handle comfortably.
* Filter evaluation happens at the SQL layer (`IN (SELECT …)` subqueries) — no JS-side row scans for set or Top N filters.
* Show Values As + value-field sort run as post-aggregation JS transforms — they operate on the already-pivoted result set, not the raw source.
* `refreshOnLoad="1"` in the XLSX export means Excel rebuilds the cache from the live source on open — saved files don't carry stale aggregate values.


# Outline & Grouping

Group rows or columns into collapsible outlines with Excel-style +/- gutter buttons and a 1/2/3 level selector

Outlining lets users collapse and expand contiguous ranges of rows or columns into nested levels — the same `Shift + Alt + →` mechanic from Excel. The state lives on `DimensionProperties` inside each sheet, the actions are exposed as hooks on `useSpreadsheetState`, and a pair of optional render components (`RowOutlineGutter`, `ColumnOutlineGutter`) paint the +/- buttons and bracket lines next to the canvas.

Outline state round-trips through both the XLSX and ODS importer/exporter, so a sheet grouped in your app opens grouped in Excel / LibreOffice and vice versa.

## Data model

Group state lives on the sheet's `rowMetadata` / `columnMetadata` arrays. Both arrays are 1-indexed (index `0` is reserved) and entries are `DimensionProperties`:

```ts
type DimensionProperties = {
  // Depth of the group this row/column belongs to (1..7).
  outlineLevel?: number;
  // True on a summary cell whose group is currently collapsed —
  // controls whether the gutter shows + or -.
  collapsed?: boolean;
  // True on a child cell when its group is collapsed. Distinct from
  // `hiddenByUser` so an outline expand doesn't unhide a manually
  // hidden row.
  hiddenByGroup?: boolean;
  // ...sizing/visibility flags
};
```

Sheet-level direction settings live in `sheet.outlinePr`:

```ts
type OutlinePr = {
  // Where the row summary sits relative to its group.
  //   true  (Excel default): summary is BELOW the group
  //   false:                  summary is ABOVE the group
  summaryBelow?: boolean;
  // Where the column summary sits.
  //   true  (Excel default): summary is to the RIGHT of the group
  //   false:                  summary is to the LEFT
  summaryRight?: boolean;
};
```

A typical level-1 row group with the level-1 summary sitting at row 6 looks like:

```ts
sheet.rowMetadata[3] = { outlineLevel: 1 };
sheet.rowMetadata[4] = { outlineLevel: 1 };
sheet.rowMetadata[5] = { outlineLevel: 1 };
// row 6 is the summary — left at outlineLevel 0
```

Collapsing that group then sets:

```ts
sheet.rowMetadata[3].hiddenByGroup = true;
sheet.rowMetadata[4].hiddenByGroup = true;
sheet.rowMetadata[5].hiddenByGroup = true;
sheet.rowMetadata[6].collapsed = true; // summary shows the "+" button
```

Nested groups stack the level — e.g. cols 3–4 inside a level-1 group spanning 2–5 carry `outlineLevel: 2`. The maximum depth is **7** (matches Excel).

## State hooks

`useSpreadsheetState` exposes nine callbacks. All of them undo/redo via the same patch history the rest of the spreadsheet uses, and emit a matching `onCommand` event so you can mirror the action to a backend.

| Callback                | Signature                                   | Behavior                                                                                                                                                                     |
| ----------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onGroupRows`           | `(sheetId, rowIndexes: number[])`           | Increments `outlineLevel` on the selected rows. Non-contiguous selections produce one group per contiguous span.                                                             |
| `onGroupColumns`        | `(sheetId, columnIndexes: number[])`        | Same, for columns.                                                                                                                                                           |
| `onUngroupRows`         | `(sheetId, rowIndexes: number[])`           | Decrements `outlineLevel`. Once a row's level reaches 0 the `collapsed` / `hiddenByGroup` flags are cleared too.                                                             |
| `onUngroupColumns`      | `(sheetId, columnIndexes: number[])`        | Same, for columns.                                                                                                                                                           |
| `onCollapseRowGroup`    | `(sheetId, summaryRowIndex)`                | Hides every row in the group that summary belongs to (respects `outlinePr.summaryBelow`).                                                                                    |
| `onCollapseColumnGroup` | `(sheetId, summaryColumnIndex)`             | Hides every column in the group that summary belongs to.                                                                                                                     |
| `onExpandRowGroup`      | `(sheetId, summaryRowIndex)`                | Reverses a collapse. Manual `hiddenByUser` flags on individual rows survive the expand.                                                                                      |
| `onExpandColumnGroup`   | `(sheetId, summaryColumnIndex)`             | Reverses a column collapse.                                                                                                                                                  |
| `onSetOutlineDepth`     | `(sheetId, axis: "row" \| "column", depth)` | Excel-style 1 / 2 / 3 selector. `depth=1` collapses every group, `depth=maxLevel+1` expands every group, intermediate values keep outer levels open and collapse inner ones. |

```tsx
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const {
  onGroupRows,
  onUngroupRows,
  onCollapseRowGroup,
  onExpandRowGroup,
  onGroupColumns,
  onUngroupColumns,
  onCollapseColumnGroup,
  onExpandColumnGroup,
  onSetOutlineDepth,
} = useSpreadsheetState({ sheets, sheetData, ... });
```

## Rendering the gutter

`@rowsncolumns/spreadsheet` ships two memoised render components — `RowOutlineGutter` and `ColumnOutlineGutter` — that paint the +/- buttons, bracket lines, and 1/2/3 level selector. They are siblings of the `CanvasGrid` (not children); each gutter is a no-op when the sheet has no groups on its axis, so it's safe to always render them.

```tsx
import {
  CanvasGrid,
  ColumnOutlineGutter,
  RowOutlineGutter,
} from "@rowsncolumns/spreadsheet";

<div className="flex flex-col h-full">
  <ColumnOutlineGutter
    sheetId={activeSheetId}
    columnMetadata={columnMetadata}
    rowMetadata={rowMetadata}
    onCollapseColumnGroup={onCollapseColumnGroup}
    onExpandColumnGroup={onExpandColumnGroup}
    onSetOutlineDepth={onSetOutlineDepth}
  />
  <div className="flex flex-1 min-h-0 min-w-0">
    <RowOutlineGutter
      sheetId={activeSheetId}
      rowMetadata={rowMetadata}
      onCollapseRowGroup={onCollapseRowGroup}
      onExpandRowGroup={onExpandRowGroup}
      onSetOutlineDepth={onSetOutlineDepth}
    />
    <CanvasGrid /* ...usual props */ />
  </div>
</div>
```

Why siblings, not children? `CanvasGrid` relies on its wrapper (`.rnc-canvas-wrapper`) being the `position: relative` ancestor for active-cell and selection overlays. Wrapping or nesting the canvas inside the gutter would break that anchor.

### Layout details

* The column gutter is a horizontal strip above the canvas; its height = `(maxColumnOutlineLevel + 1) * 20 + 4` px.
* The row gutter is a vertical strip to the left of the canvas; its width = `(maxRowOutlineLevel + 1) * 20 + 4` px.
* `computeOutlineGutterSize(metadata)` returns the same value if you need to inset something else.
* The column gutter scrolls horizontally in lockstep with the canvas (via the `scrollSubscriber` signal); the row gutter scrolls vertically.

### Direction (summaryBelow / summaryRight)

Pass the sheet's direction through to flip where the +/- button sits:

```tsx
<RowOutlineGutter
  ...
  summaryBelow={sheet.outlinePr?.summaryBelow !== false}
/>
<ColumnOutlineGutter
  ...
  summaryRight={sheet.outlinePr?.summaryRight !== false}
/>
```

Both default to `true` (Excel's default).

## Keyboard shortcuts

Outline shortcuts are wired through the same `keyboard-handler` the rest of the canvas uses:

| Shortcut          | Action                          |
| ----------------- | ------------------------------- |
| `Shift + Alt + →` | Group selected rows / columns   |
| `Shift + Alt + ←` | Ungroup selected rows / columns |

## Import / Export

| Format | Import | Export | Round-trip tests                                                    |
| ------ | ------ | ------ | ------------------------------------------------------------------- |
| XLSX   | ✅      | ✅      | `libs/toolkit/excel-parser/__tests__/outline-roundtrip.spec.ts`     |
| ODS    | ✅      | ✅      | `libs/toolkit/excel-parser/__tests__/ods-outline-roundtrip.spec.ts` |

### XLSX details

* Children: `<row outlineLevel="N" hidden="1" collapsed="…">`, `<col outlineLevel="N" …>`.
* Sheet direction: `<sheetPr><outlinePr summaryBelow="…" summaryRight="…"/></sheetPr>` — only emitted when the sheet diverges from Excel's defaults.
* `hiddenByGroup` is encoded as `hidden="1"` + `outlineLevel > 0`; the parser routes it back into `hiddenByGroup` rather than `hiddenByUser` so a user-driven hide isn't conflated with a group hide.

### ODS details

* Groups are wrapped in `<table:table-row-group>` / `<table:table-column-group>` elements, nested for level > 1.
* A collapsed group carries `table:display="false"`; rows/columns inside such a wrapper get `hiddenByGroup: true`, and the summary cell (per `outlinePr.summaryBelow` / `summaryRight`) gets `collapsed: true`.
* ODS has no native equivalent of `<outlinePr>`. Non-default `summaryBelow` / `summaryRight` are **not** persisted through an ODS round-trip — the sheet reopens with Excel defaults.

## Notes & gotchas

* **Maximum depth is 7.** `onGroupRows` / `onGroupColumns` clamp at level 7; further `Group` actions on already-deep rows are no-ops.
* **Manual hide survives an expand.** `useOnExpandRowGroup` / `useOnExpandColumnGroup` only clear `hiddenByGroup`; `hiddenByUser` from `onHideRow` / `onHideColumn` is preserved. This is better than Excel, which conflates the two.
* **Depth selector and nested groups.** `onSetOutlineDepth` decides hiding from each cell's own `outlineLevel`, so a level-2 child stays hidden when the surrounding level-1 group expands. (Earlier versions wrote `hiddenByGroup` on whole spans during the scan, which caused the outer-group expand to clobber the inner-group collapse.)
* **Overlapping groups are not supported.** The data model uses `outlineLevel` as a per-cell depth, not a group ID, so groups must be **nested or disjoint** — same constraint Excel has. Re-grouping a range that overlaps an existing group nests it inside the larger group.


# Print preview

Google-Sheets-style full-screen print preview dialog with paginated previews, paper-size + margin + orientation settings, headers/footers, scope, and a paginated browser print

`PrintPreviewDialog` is a self-contained, full-screen modal that paginates the active sheet into printable page-sized blocks, renders the same blocks into a hidden iframe with the appropriate `@page` rules, and triggers the browser print dialog. Modelled on Google Sheets' Print pane — same sidebar controls, same paginated preview area, same paper sizes.

## Is this a breaking change?

**No.** Print preview is purely additive:

* New named export from `@rowsncolumns/spreadsheet`: `PrintPreviewDialog` and `PrintSettings` / `PaperSizeId` / related types.
* `ButtonPrint`'s API is unchanged — it's still a thin `ToolbarIconButton`. The only thing that updates is **your `onClick` handler** (you used to call `window.print()`, now you open the dialog).
* No props on `useSpreadsheetState`, `CanvasGrid`, or any existing component change. If you don't render `PrintPreviewDialog`, nothing happens.

Upgrade path: drop the new dialog into your app and rewire `ButtonPrint`. That's it.

## Why a dialog instead of `window.print()`?

`window.print()` prints the **entire host page** — your toolbar, formula bar, sidebar, panels, ads, whatever else lives on the route. That's almost never what a user wants when they hit ⌘P on a spreadsheet.

The dialog instead:

1. Paginates the **sheet data** into page-sized blocks at the chosen paper size + orientation + scale.
2. Renders each block as a self-contained HTML table styled from `getEffectiveFormat`.
3. On **Print**, snapshots only those blocks into a hidden iframe alongside the matching `@page { size: ... }` CSS, then calls `iframe.contentWindow.print()`. The browser print dialog shows the actual paginated spreadsheet, nothing else from your app.

## Integration

The dialog reads everything it needs from values you already have on `useSpreadsheetState`. No adapter object; the props are flat.

```tsx
import { useState, useEffect } from "react";
import {
  PrintPreviewDialog,
  ButtonPrint,
} from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function App() {
  const {
    sheets,
    activeSheetId,
    selections,
    getFormattedValue,
    getEffectiveFormat,
    getDataRowCount,
    getDataColumnCount,
    getCellData,
  } = useSpreadsheetState({ /* ... */ });

  const [isPrintOpen, setPrintOpen] = useState(false);

  // ⌘P / Ctrl+P hijack + Esc dismiss. Kept in user space rather than
  // baking into `PrintPreviewDialog` so an embedding app can decide
  // whether Esc should dismiss other overlays first.
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "p") {
        e.preventDefault();
        setPrintOpen(true);
        return;
      }
      if (e.key === "Escape" && isPrintOpen) {
        e.preventDefault();
        setPrintOpen(false);
      }
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, [isPrintOpen]);

  return (
    <>
      <ButtonPrint onClick={() => setPrintOpen(true)} />

      <PrintPreviewDialog
        isOpen={isPrintOpen}
        onClose={() => setPrintOpen(false)}
        title="My Workbook"
        sheets={sheets}
        activeSheetId={activeSheetId}
        selections={selections}
        getFormattedValue={getFormattedValue}
        getEffectiveFormat={getEffectiveFormat}
        getDataRowCount={getDataRowCount}
        getDataColumnCount={getDataColumnCount}
        getCellData={getCellData}
      />
    </>
  );
}
```

That's the whole integration. The dialog manages its own settings state internally — the host doesn't need to persist or store anything. Settings carry over across open/close cycles within the same mount, so a user who picks A4 + Landscape and reopens still sees A4 + Landscape. If you want every open to start fresh, pass a changing `key` (e.g. `key={isPrintOpen ? "open" : "closed"}`) — React will remount the component and reset state to defaults.

## `PrintPreviewDialog` props

| Prop                 | Type                                | Required | Purpose                                                                                                                                                |
| -------------------- | ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isOpen`             | `boolean`                           | ✅        | Controlled visibility.                                                                                                                                 |
| `onClose`            | `() => void`                        | ✅        | Fires on Cancel, the close icon, or after Print. (Esc binding lives in user space — see Integration above.)                                            |
| `sheets`             | `Sheet[]`                           | ✅        | Full sheet list — same array on `useSpreadsheetState.sheets`. Drives both the active-sheet lookup and the "Workbook" scope.                            |
| `activeSheetId`      | `number`                            | ✅        | Which sheet to preview. The dialog looks it up in `sheets`.                                                                                            |
| `selections`         | `SelectionArea[]?`                  |          | The first selection's range powers the "Selection" scope. Pass `useSpreadsheetState.selections` straight through; omit / pass `[]` to hide the option. |
| `title`              | `string?`                           |          | Workbook display name. Shown in the header strip when the "Workbook title" header field is enabled.                                                    |
| `getFormattedValue`  | `(sheetId, row, col) => string`     | ✅        | From `useSpreadsheetState`.                                                                                                                            |
| `getEffectiveFormat` | `(sheetId, row, col) => CellFormat` | ✅        | From `useSpreadsheetState`.                                                                                                                            |
| `getDataRowCount`    | `(sheetId) => number?`              |          | When present, restricts the print extent to the last non-empty row (matches Google Sheets). Falls back to `sheet.rowCount`.                            |
| `getDataColumnCount` | `(sheetId) => number?`              |          | Same as above for columns.                                                                                                                             |
| `getCellData`        | `(sheetId, row, col) => CellData?`  |          | Surfaces the "Show notes" red corner marker. Without it the toggle still renders but produces no markers.                                              |

The dialog pulls everything else off the `Sheet` it resolves — `title` (display name), `merges`, `frozenRowCount` / `frozenColumnCount`, and per-row / per-column sizes from `rowMetadata` / `columnMetadata`.

## Sidebar settings

The dialog manages its own settings state — defaults match Google Sheets where they line up.

| Setting                   | Options                                                                                                        |
| ------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Print scope**           | Current sheet · Workbook (when `sheets.length > 1`) · Selection (when `selections[0]` is set)                  |
| **Paper size**            | Letter · Tabloid · Legal · Statement · Executive · Folio · A3 · A4 · A5 · B4 · B5                              |
| **Orientation**           | Portrait · Landscape                                                                                           |
| **Scale**                 | Normal (100 %) · Fit to width · Fit to page · Custom (10 – 400 %)                                              |
| **Margins**               | Normal · Narrow · Wide · Custom (per-edge px)                                                                  |
| **Show gridlines**        | toggle                                                                                                         |
| **Show notes**            | toggle — paints a red corner triangle on noted cells (needs `getCellData`)                                     |
| **Page order**            | Over, then down (default) · Down, then over                                                                    |
| **Horizontal alignment**  | Left · Center · Right                                                                                          |
| **Vertical alignment**    | Top · Middle · Bottom                                                                                          |
| **Headers & footers**     | Page numbers · Workbook title · Sheet name · Current date · Current time — auto-placed across the 3-zone strip |
| **Repeat frozen rows**    | toggle, disabled when `Sheet.frozenRowCount` is 0 / unset                                                      |
| **Repeat frozen columns** | toggle, disabled when `Sheet.frozenColumnCount` is 0 / unset                                                   |

## Pagination

The pagination engine (`computePageLayout`) walks the cell range with the active scale, splitting rows and columns into bands whose summed scaled dimensions fit the page content area (paper minus margins minus optional header/footer strips, minus a 1 px reserve for the closing cell border).

* `Over, then down`: outer = row bands, inner = column bands — page order goes left-to-right across the first row band, then advances. (Google Sheets' default.)
* `Down, then over`: outer = column bands, inner = row bands — page order goes top-to-bottom within the first column band, then advances. (Excel's `pageOrder="downThenOver"` default.)

Frozen rows / columns, when their toggle is on, are subtracted from the content area on every page and prepended as a fixed prefix to each band — every page renders with the same frozen header rows / leftmost columns.

## What's NOT in this release

Deferred so we could ship the core flow. None block the integration above.

* **Set custom page breaks** — drag-edit page breaks UI. The underlying `<rowBreaks>` / `<colBreaks>` XLSX metadata still round-trips on import/export.
* **Edit custom fields** — the Google-Sheets 3-zone (`&L` / `&C` / `&R`) header / footer composer. v1 auto-places enabled fields.
* **Show notes — note text** — the toggle ships and marks noted cells with a red corner triangle, but the note text isn't emitted in a trailing endnote section (Google Sheets does this; deferred).

## Files

* `apps/spreadsheet/components/print-preview/settings.ts` — `PrintSettings` model + paper-size / margin constants.
* `apps/spreadsheet/components/print-preview/use-page-layout.ts` — pure pagination math; band slicing, scale resolution.
* `apps/spreadsheet/components/print-preview/cell-styles.ts` — maps `CellFormat` to inline CSS for the printed cells.
* `apps/spreadsheet/components/print-preview/page-block.tsx` — renders one page as a styled HTML table.
* `apps/spreadsheet/components/print-preview/print-preview-dialog.tsx` — the modal, sidebar, and pagination orchestration.
* `apps/spreadsheet/components/print-preview/run-print.ts` — hidden-iframe `window.print()` driver with `@page` rules.


# Tokenizer

A Tokenizer tells the Spreadsheet how to identify formulas and string tokens in the user entered value of cell.

Spreadsheet comes with a built-in tokenizer in `rowsncolumns/calculator` module. Developers can pass a custom tokenizer, especially if you want to additional highlighting capabilities.


# Lazy loading/Infinite scrolling

Load data based on the visible viewport of the grid

CanvasGrid invokes `onViewPortChange` callback whenever user scrolls or pans around the spreadsheet.

This can be used to load data for the next set of visible rows.

{% hint style="info" %}
Do note that lazy loading can affect calculations if cell dependents are out of the viewport and not loaded initially.

We recommend lazy loading only if calculations are moved to the server side.
{% endhint %}

{% code overflow="wrap" %}

```tsx
import { CanvasGrid } from "@rowsncolumns/spreadsheet"

const App = () => {
  const [ sheetData, onChangeSheetData ] = useState<SheetData<T>>({})
  <CanvasGrid
    onViewPortChange={(viewport: ViewPortProps) => {
      const {
        columnStartIndex, 
        columnStopIndex,
        rowStartIndex,
        rowStopIndex,
        visibleColumnStartIndex,
        visibleColumnStopIndex,
        visibleRowStartIndex,
        visibleRowStopIndex      
      } = viewport
      
      // Throttle fetch request
      // Move it out of the render loop, this is just an example
      throttle(
        fetch(`/rows?start=${rowStartIndex}&end=${rowStopIndex}`)
          .then(rowData => {
            // Append new row data
            onChangeSheetData(prev => {
              const newRowData = prev[sheetId]
                  .splice(rowStartIndex, rowStopIndex - rowStartIndex, ...rowData)
              return {
                ...prev,
                [sheetId]: newRowData
              }
            })
          })
      , 300)
    }}
  />
}
```

{% endcode %}

## Hooks for Lazy loading

`@rowsncolumns/spreadsheet-state` exports the `useAsyncDatasource` hook to load paged data for the current viewport while caching nearby pages.

### Usage

```tsx
import { CanvasGrid, CellData, SpreadsheetProvider } from "@rowsncolumns/spreadsheet";
import { RowData, SheetData, useAsyncDatasource } from "@rowsncolumns/spreadsheet-state";
import { CircularLoader } from "@rowsncolumns/ui";

const App = () => {
  const locale = "en-US";
  const [sheetData, onChangeSheetData] = useState<SheetData<CellData>>({});
  const pageSize = 100;
  const rowCount = 100_000;
  const columnCount = 100;
  const sheetId = 1;

  const buildRow = (rowIndex: number): RowData<CellData | null> => {
    const values: (CellData | null)[] = Array.from(
      { length: columnCount + 1 },
      () => null
    );
    values[1] = {
      ue: { sv: `Row ${rowIndex}` },
      fv: `Row ${rowIndex}`,
    };
    values[2] = {
      ue: { nv: rowIndex },
      fv: String(rowIndex),
    };
    values[3] = {
      ue: { sv: `Page ${Math.floor(rowIndex / pageSize)}` },
      fv: `Page ${Math.floor(rowIndex / pageSize)}`,
    };
    return { values };
  };

  const getRowData = useCallback(
    async (_sheetId: number, [rowStartIndex, rowStopIndex]: [number, number]) => {
      await new Promise((res) => setTimeout(res, 200));
      const rows: RowData<CellData | null>[] = [];
      for (let rowIndex = rowStartIndex; rowIndex < rowStopIndex; rowIndex++) {
        rows.push(buildRow(rowIndex));
      }
      return rows;
    },
    [pageSize]
  );

  const { onViewPortChange, isLoading } = useAsyncDatasource<CellData>({
    sheetId,
    locale,
    pageSize,
    bufferPages: 2,
    maxCachedPages: 6,
    rowCount,
    getRowData,
    onChangeSheetData,
  });
  
  return (
    <SpreadsheetProvider>
      <div className="relative flex min-h-[70vh] flex-1">
        <CanvasGrid<CellData>
          sheetId={sheetId}
          rowCount={rowCount}
          columnCount={columnCount}
          onViewPortChange={onViewPortChange}
          getCellData={(targetSheetId, rowIndex, columnIndex) =>
            sheetData?.[targetSheetId]?.[rowIndex]?.values?.[columnIndex]
          }
          readonly
          licenseKey="evaluation-license"
        />
        {isLoading ? (
          <CircularLoader className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10" />
        ) : null}
      </div>
    </SpreadsheetProvider>
  );
}
```

Notes:

* `bufferPages` keeps a cushion of pages around the viewport.
* `maxCachedPages` caps total cached pages and evicts the farthest ones.
* `rowCount` prevents over-fetching beyond your backend total.


# OpenAI/Chat GPT Integration

Built-in OpenAI integration using ASK\_OPENAI function

Open AI integration is available as a separate npm package

{% tabs %}
{% tab title="Yarn" %}

```sh
yarn add @rowsncolumns/openai
```

{% endtab %}

{% tab title="NPM" %}

```sh
npm install @rowsncolumns/openai
```

{% endtab %}
{% endtabs %}

### Usage

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet"
import {
  ASK_OPENAI,
  functionDescriptions as openAIFunctionDescriptions,
} from "@rowsncolumns/openai";
import { functionDescriptions, functions } from "@rowsncolumns/functions";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state"


const allFunctions = {
  ...functions,
  ASK_OPENAI
}

const allFunctionDescriptions = functionDescriptions.concat(
  openAIFunctionDescriptions
);

const MySpreadsheet = () => {
  const { ... } = useSpreadsheetState({
   functions: allFunctions
  })
  return (
    <CanvasGrid
      functionDescriptions={allFunctionDescriptions}
      {...}
    />
  )
}

export const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
)

```

<figure><img src="/files/xH92lFczvTfEB4aLxSaD" alt=""><figcaption><p>Example of Open AI Integration</p></figcaption></figure>


# Search

Use the provided hooks and components to quickly add search functionality

`useSearch` hook gives you some basic defaults for search and outputs a `borderStyles` prop so that the grid can highlight the cells.

`SheetSearch` is a generic form input that shows and hides when search is active.

<pre class="language-typescript" data-overflow="wrap"><code class="lang-typescript"><strong>import { SpreadsheetProvider, CanvasGrid, SheetSearch } from "@rowsncolumns/spreadsheet";
</strong>import { useSearch } from "@rowsncolumns/spreadsheet-state"

const MySpreadsheet = () => {
  const { getCellData, getNonEmptyColumnCount, getNonEmptyRowCount } = useSpreadsheetState()
  
  const {
    onSearch,
    onResetSearch,
    onFocusNextResult,
    onFocusPreviousResult,
    hasNextResult,
    hasPreviousResult,
    borderStyles,
    isSearchActive,
    onRequestSearch,
  } = useSearch({
    getCellData,
    sheetId: activeSheetId,
    getNonEmptyColumnCount,
    getNonEmptyRowCount,
  });
  
  return (
    &#x3C;>
      &#x3C;CanvasGrid
        ...
        onRequestSearch={onRequestSearch}
        borderStyles={borderStyles}
      />
      
      // Make sure you have a parent div with position: relative
      &#x3C;SheetSearch
        isActive={isSearchActive}
        onSubmit={onSearch}
        onReset={onResetSearch}
        onNext={onFocusNextResult}
        onPrevious={onFocusPreviousResult}
        disableNext={!hasNextResult}
        disablePrevious={!hasPreviousResult}
      />
    &#x3C;/>
  );
};

const App = () => (
  &#x3C;SpreadsheetProvider>
    &#x3C;MySpreadsheet />
  &#x3C;/SpreadsheetProvider>
);
</code></pre>


# Formula protection

Add extra layer of protection to prevent users from copying formulas

Enabling formula protection mode prevents users from viewing, copying or editing formulas on the Spreadsheet

This is useful if you want to share a Spreadsheet with the team, but not give them access to formulas due to confidential reasons

To enable protection mode, set `protectFormulas={true}`

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const MySpreadsheet = () => {
  const { getCellData } = useSpreadsheetState()

  
  return (
    <CanvasGrid
      protectFormulas={true}
    />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```


# Autofill

Automatically fill a series of data in the Spreadsheet

Spreadsheet 2 does a series autofill based on the value of selected cells. This can be customized, if you need autofill to be powered by AI or an async API

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const MySpreadsheet = () => {
  const {  } = useSpreadsheetState({
    getAutoFillValues: async ({
      sheetId,
      direction,
      selection,
      fillBounds,
      getCellData,
      locale
    }) => {
      // return an array of cellData
    }
  })

  
  return (
    <CanvasGrid />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```


# Paint Format

Copy formatting from one cell or range and apply it to another

The Paint Format feature, also known as Format Painter, allows users to copy formatting from one cell or range and apply it to another location. This is useful for quickly applying consistent formatting across your spreadsheet.

## Overview

Paint Format copies all visual formatting from the source cells (colors, borders, fonts, alignment, number formats, etc.) and applies it to target cells, without affecting the cell values.

## Basic Usage

### Using the Toolbar Button

The easiest way to use Paint Format is through the `ButtonPaintFormat` toolbar button:

```tsx
import {
  SpreadsheetProvider,
  CanvasGrid,
  Toolbar,
  ButtonPaintFormat,
} from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function MySpreadsheet() {
  const {
    activeCell,
    activeSheetId,
    selections,
    onSavePaintFormat,
    isPaintFormatActive,
    // ... other hook values
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets,
    onChangeSheetData,
  });

  return (
    <SpreadsheetProvider>
      <Toolbar>
        <ButtonPaintFormat
          isActive={isPaintFormatActive}
          onClick={() =>
            onSavePaintFormat(activeSheetId, activeCell, selections)
          }
        />
      </Toolbar>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

## How It Works

### 1. Copy Format (Activate Paint Format)

Click the Paint Format button or call `onSavePaintFormat` to copy formatting from the currently selected cells:

```tsx
// Save formatting from current selection
onSavePaintFormat(activeSheetId, activeCell, selections);
```

When paint format is active, `isPaintFormatActive` returns `true`, which can be used to show visual feedback.

### 2. Apply Format

Once activated, simply select the target cells where you want to apply the formatting. The spreadsheet automatically applies the copied formatting to the new selection.

### 3. Deactivate

Paint format automatically deactivates after applying to one selection. To cancel without applying, click the Paint Format button again.

## Programmatic Usage

You can also use paint format programmatically:

```tsx
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const {
  onSavePaintFormat,
  onApplyPaintFormat,
  isPaintFormatActive,
  paintFormat, // Direct function to paint from one range to another
} = useSpreadsheetState({
  // ... configuration
});

// Copy formatting from range A1:B2
onSavePaintFormat(
  1, // sheetId
  { rowIndex: 1, columnIndex: 1 }, // activeCell
  [{
    range: {
      startRowIndex: 1,
      endRowIndex: 2,
      startColumnIndex: 1,
      endColumnIndex: 2,
    }
  }]
);

// Or paint directly from one range to another
paintFormat(
  {
    startRowIndex: 1,
    endRowIndex: 2,
    startColumnIndex: 1,
    endColumnIndex: 2,
    sheetId: 1,
  },
  {
    startRowIndex: 5,
    endRowIndex: 6,
    startColumnIndex: 3,
    endColumnIndex: 4,
    sheetId: 1,
  }
);
```

## What Gets Copied

Paint Format copies the following formatting properties:

* **Text formatting**: Font family, font size, bold, italic, underline, strikethrough
* **Colors**: Text color, background color
* **Borders**: All border styles and colors
* **Alignment**: Horizontal and vertical alignment
* **Number formats**: Currency, percentage, date, custom formats
* **Text wrapping**: Wrap, overflow, or clip
* **Indentation**: Text indentation levels

## What Does NOT Get Copied

Paint Format does NOT copy:

* Cell values or formulas
* Cell comments/notes
* Data validation rules
* Conditional formatting rules
* Protected range settings
* Merged cell configuration

## Complete Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Toolbar,
  ButtonPaintFormat,
  ToolbarSeparator,
  ButtonBold,
  ButtonItalic,
  BackgroundColorSelector,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  type SheetData,
} from "@rowsncolumns/spreadsheet-state";

function SpreadsheetWithPaintFormat() {
  const [sheets, setSheets] = useState([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet 1" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    getEffectiveFormat,
    onChangeActiveCell,
    onChangeSelections,
    onChangeFormatting,
    onSavePaintFormat,
    isPaintFormatActive,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  const currentCellFormat = getEffectiveFormat(
    activeSheetId,
    activeCell.rowIndex,
    activeCell.columnIndex
  );

  return (
    <SpreadsheetProvider>
      <Toolbar>
        <ButtonBold
          isActive={currentCellFormat?.textFormat?.bold}
          onClick={() =>
            onChangeFormatting(
              activeSheetId,
              activeCell,
              selections,
              "textFormat",
              { bold: !currentCellFormat?.textFormat?.bold }
            )
          }
        />
        <ButtonItalic
          isActive={currentCellFormat?.textFormat?.italic}
          onClick={() =>
            onChangeFormatting(
              activeSheetId,
              activeCell,
              selections,
              "textFormat",
              { italic: !currentCellFormat?.textFormat?.italic }
            )
          }
        />
        <BackgroundColorSelector
          color={currentCellFormat?.backgroundColor}
          onChange={(color) =>
            onChangeFormatting(
              activeSheetId,
              activeCell,
              selections,
              "backgroundColor",
              color
            )
          }
        />
        <ToolbarSeparator />
        <ButtonPaintFormat
          isActive={isPaintFormatActive}
          onClick={() =>
            onSavePaintFormat(activeSheetId, activeCell, selections)
          }
        />
      </Toolbar>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
        onChangeActiveCell={onChangeActiveCell}
        onChangeSelections={onChangeSelections}
      />
    </SpreadsheetProvider>
  );
}
```

## Keyboard Shortcut

Paint Format can also be activated via keyboard shortcuts (implementation dependent on your configuration):

```tsx
// Example: Activate paint format with Ctrl+Shift+C (copy format)
// Then apply with Ctrl+Shift+V (paste format)
```

## Use Cases

### Consistent Table Headers

Apply the same header formatting to multiple tables:

1. Format one header cell (bold, background color, borders)
2. Click Paint Format
3. Select other header cells to apply the same style

### Standardizing Reports

Quickly apply consistent formatting across similar data sections:

1. Format one complete section
2. Use Paint Format to copy it
3. Apply to other sections

### Color Coding

Apply color coding patterns to categorize data:

1. Set up one cell with specific colors
2. Paint format to similar categories

## Best Practices

1. **Format once, apply many**: Create template cells with desired formatting, then use Paint Format to replicate
2. **Visual feedback**: The `isPaintFormatActive` state should be used to provide visual indication that paint format mode is active
3. **Undo support**: Paint format operations are tracked in undo history
4. **Performance**: Paint format is efficient even for large ranges

## API Reference

### useSpreadsheetState Returns

| Property              | Type                                        | Description                                  |
| --------------------- | ------------------------------------------- | -------------------------------------------- |
| `onSavePaintFormat`   | `(sheetId, activeCell, selections) => void` | Saves formatting from selection for painting |
| `onApplyPaintFormat`  | `(sheetId, activeCell, selections) => void` | Applies saved formatting to new selection    |
| `isPaintFormatActive` | `boolean`                                   | Whether paint format mode is active          |
| `paintFormat`         | `(sourceRange, targetRange) => void`        | Directly paint from one range to another     |

## Limitations

* Paint format is a client-side operation and works with the current spreadsheet state
* Very large ranges may take a moment to process
* Formatting from merged cells requires special handling

## Troubleshooting

### Paint Format Not Working

Ensure you have the necessary callbacks:

```tsx
const {
  onSavePaintFormat,
  isPaintFormatActive,
  // These are required for paint format to work
  onChangeFormatting,
  getEffectiveFormat,
} = useSpreadsheetState({
  // configuration
});
```

### Visual Feedback Not Showing

Use `isPaintFormatActive` to style the button:

```tsx
<ButtonPaintFormat
  isActive={isPaintFormatActive}  // This provides visual feedback
  onClick={handlePaintFormat}
/>
```


# Advanced Grid Features

AI-powered autofill and advanced grid navigation features

This document covers advanced features available in the CanvasGrid component that enhance user interaction and productivity.

## Magic Fill

Magic Fill is an AI-powered autofill feature that intelligently predicts and fills data based on patterns. When enabled, it enhances the standard autofill functionality with smart pattern recognition.

### Enabling Magic Fill

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function MySpreadsheet() {
  const {
    activeCell,
    activeSheetId,
    selections,
    // ... other hook values
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets,
    onChangeSheetData,
  });

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        enableMagicFill={true}  // Enable magic fill
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

### How It Works

Magic Fill analyzes the selected data and intelligently fills the remaining cells:

1. User selects a range of cells with data
2. User drags the fill handle or uses autofill
3. Magic Fill analyzes the pattern and fills intelligently
4. Can handle complex patterns, dates, text series, and more

### Use Cases

* **Smart series completion**: Fills dates, numbers, or text patterns
* **Data transformation**: Applies consistent transformations across cells
* **Pattern recognition**: Detects and continues complex patterns

## Data Boundary Navigation

Data Boundary Navigation allows users to quickly jump to the edges of data ranges using keyboard shortcuts (typically Ctrl+Arrow keys).

### Enabling Data Boundary Navigation

```tsx
<CanvasGrid
  enableDataBoundaryNavigation={true}
  // ... other props
/>
```

### How It Works

When enabled, users can:

* **Ctrl+Arrow Up**: Jump to the first non-empty cell or edge of data above
* **Ctrl+Arrow Down**: Jump to the last non-empty cell or edge of data below
* **Ctrl+Arrow Left**: Jump to the first non-empty cell or edge of data to the left
* **Ctrl+Arrow Right**: Jump to the last non-empty cell or edge of data to the right

This feature mimics Excel's behavior and is essential for navigating large datasets efficiently.

### Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
} from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function SpreadsheetWithNavigation() {
  const [sheets, setSheets] = useState([
    { sheetId: 1, rowCount: 1000, columnCount: 26, title: "Data" }
  ]);
  const [sheetData, setSheetData] = useState({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
        onChangeActiveCell={onChangeActiveCell}
        onChangeSelections={onChangeSelections}
        enableDataBoundaryNavigation={true}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

## Selection Resize Handles

Selection resize handles provide visual indicators and interaction points for resizing selections, similar to Excel's selection handles.

### Enabling Resize Handles

```tsx
<CanvasGrid
  showSelectionResizeHandles={true}
  // ... other props
/>
```

### Features

* **Visual handles**: Small squares at the corners and edges of selections
* **Click and drag**: Resize selections by dragging handles
* **Fill handle**: Special handle at the bottom-right for autofill operations
* **Multi-selection support**: Works with multiple selections

### Usage Example

```tsx
<CanvasGrid
  sheetId={activeSheetId}
  activeCell={activeCell}
  selections={selections}
  showSelectionResizeHandles={true}  // Show resize handles
  onFill={onFill}  // Required for fill handle functionality
  onChangeSelections={onChangeSelections}  // Required for resizing
  // ... other props
/>
```

## Readonly Mode

Readonly mode disables all editing functionality while still allowing viewing and navigation.

### Enabling Readonly Mode

```tsx
<CanvasGrid
  readonly={true}
  // ... other props
/>
```

### Features When Readonly

* **No editing**: Users cannot modify cell values
* **Navigation enabled**: Arrow keys and scrolling still work
* **Selection allowed**: Users can select cells and ranges
* **Copy enabled**: Users can copy data
* **Formulas visible**: Formula bar shows cell formulas (read-only)

### Use Cases

* **Viewing reports**: Display data without modification risk
* **Approval workflows**: Show data for review before editing
* **Public dashboards**: Share spreadsheets in view-only mode
* **Template preview**: Show template structure before using

### Example

```tsx
import { useState } from "react";
import { CanvasGrid, SpreadsheetProvider } from "@rowsncolumns/spreadsheet";

function ReadonlySpreadsheet({ data, allowEditing = false }) {
  return (
    <SpreadsheetProvider>
      <div>
        <p>{allowEditing ? "Edit Mode" : "View Only"}</p>
        <CanvasGrid
          readonly={!allowEditing}
          // ... other props
        />
      </div>
    </SpreadsheetProvider>
  );
}
```

## Complete Example with All Features

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Toolbar,
  BottomBar,
  SheetTabs,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  type SheetData,
} from "@rowsncolumns/spreadsheet-state";

function AdvancedSpreadsheet() {
  const [sheets, setSheets] = useState([
    { sheetId: 1, rowCount: 1000, columnCount: 26, title: "Sheet 1" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData>({});
  const [readonly, setReadonly] = useState(false);

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
    onChangeActiveSheet,
    onFill,
    // ... other hook values
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  return (
    <SpreadsheetProvider>
      <Toolbar>
        <button onClick={() => setReadonly(!readonly)}>
          {readonly ? "Enable Editing" : "Make Readonly"}
        </button>
      </Toolbar>

      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
        onChangeActiveCell={onChangeActiveCell}
        onChangeSelections={onChangeSelections}
        onFill={onFill}
        
        // Advanced features
        enableMagicFill={true}
        enableDataBoundaryNavigation={true}
        showSelectionResizeHandles={true}
        readonly={readonly}
      />

      <BottomBar>
        <SheetTabs
          sheets={sheets}
          activeSheetId={activeSheetId}
          onChangeActiveSheet={onChangeActiveSheet}
          readonly={readonly}
        />
      </BottomBar>
    </SpreadsheetProvider>
  );
}
```

## CanvasGrid Props Reference

| Prop                           | Type      | Default | Description                                     |
| ------------------------------ | --------- | ------- | ----------------------------------------------- |
| `enableMagicFill`              | `boolean` | `false` | Enable AI-powered intelligent autofill          |
| `enableDataBoundaryNavigation` | `boolean` | `false` | Enable Ctrl+Arrow navigation to data boundaries |
| `showSelectionResizeHandles`   | `boolean` | `false` | Show visual handles for resizing selections     |
| `readonly`                     | `boolean` | `false` | Disable all editing functionality               |

## Best Practices

### Magic Fill

1. **Test with your data**: Magic fill works best with consistent patterns
2. **Provide examples**: Give at least 2-3 examples for best results
3. **Review results**: Always review magic-filled data for accuracy

### Data Boundary Navigation

1. **Large datasets**: Essential for spreadsheets with thousands of rows
2. **Keyboard-first users**: Great for power users who prefer keyboard navigation
3. **Combine with other shortcuts**: Works well with Shift+Ctrl+Arrow for selecting ranges

### Selection Resize Handles

1. **Visual clarity**: Makes it clear what is selected
2. **Touch interfaces**: Especially useful on touch devices
3. **Performance**: May impact performance with very large selections

### Readonly Mode

1. **Clear indication**: Show visual feedback that sheet is readonly
2. **Toggle capability**: Provide a way to switch between readonly and edit modes
3. **Permissions**: Use with authentication/authorization systems

## Performance Considerations

* **Magic Fill**: May have slight delay for complex pattern detection
* **Large selections**: Resize handles on very large selections (1000+ cells) may impact performance
* **Data boundary navigation**: Optimized for large datasets

## Browser Compatibility

All features work in modern browsers:

* Chrome/Edge 90+
* Firefox 88+
* Safari 14+

## Troubleshooting

### Magic Fill Not Working

Ensure you have autofill callback implemented:

```tsx
const { onFill } = useSpreadsheetState({
  // configuration
});

<CanvasGrid
  enableMagicFill={true}
  onFill={onFill}  // Required
/>
```

### Data Boundary Navigation Not Responding

Check that the feature is enabled and you have cell data:

```tsx
<CanvasGrid
  enableDataBoundaryNavigation={true}
  getCellData={getCellData}  // Required to detect boundaries
/>
```

### Selection Handles Not Visible

Verify the prop is set and selections exist:

```tsx
<CanvasGrid
  showSelectionResizeHandles={true}
  selections={selections}  // Must have active selections
/>
```


# Export canvas as image

Export the entire visible canvas or part of the canvas as an image

The spreadsheet provides functionality to export regions of the canvas as images, useful for generating reports, sharing data visualizations, or creating documentation.

## Exporting a Sheet Region

You can export specific regions of your spreadsheet as PNG or JPEG images using the `exportRegion` function from the `useSpreadsheet` hook.

### Basic Usage

{% code overflow="wrap" %}

```tsx
import { SpreadsheetProvider, CanvasGrid, useSpreadsheet } from "@rowsncolumns/spreadsheet";

const MySpreadsheet = () => {
  const { exportRegion } = useSpreadsheet()

  const handleExport = () => {
    exportRegion?.(
      {
        startRowIndex: 1,
        endRowIndex: 10,
        startColumnIndex: 1,
        endColumnIndex: 5,
      },
      "my-spreadsheet-export",  // filename (without extension)
      "image/png"                // MIME type
    );
  };
  
  return (
    <>
      <button onClick={handleExport}>
        Export Region as Image
      </button>
      <CanvasGrid />
    </>
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```

{% endcode %}

### Export Options

#### Image Format

You can export in two formats:

* **PNG** (default): `"image/png"` - Lossless, supports transparency
* **JPEG**: `"image/jpeg"` - Compressed, smaller file size

```tsx
// Export as PNG
exportRegion?.(range, "export", "image/png");

// Export as JPEG
exportRegion?.(range, "export", "image/jpeg");
```

#### Range Selection

The range object specifies which cells to include in the export:

```tsx
const range = {
  startRowIndex: 1,   // Starting row (1-indexed, 0 is header)
  endRowIndex: 20,    // Ending row (inclusive)
  startColumnIndex: 1,  // Starting column (1-indexed, 0 is header)
  endColumnIndex: 10,   // Ending column (inclusive)
};

exportRegion?.(range, "quarterly-report", "image/png");
```

### Export Current Selection

You can export the currently selected range:

```tsx
import { useSpreadsheet } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const MySpreadsheet = () => {
  const { exportRegion } = useSpreadsheet();
  const { selections, activeCell } = useSpreadsheetState({
    // ... configuration
  });

  const exportSelection = () => {
    const selection = selections.length > 0
      ? selections[selections.length - 1].range
      : {
          startRowIndex: activeCell.rowIndex,
          endRowIndex: activeCell.rowIndex,
          startColumnIndex: activeCell.columnIndex,
          endColumnIndex: activeCell.columnIndex,
        };

    exportRegion?.(selection, "selected-area", "image/png");
  };

  return <button onClick={exportSelection}>Export Selection</button>;
};
```

### Export with Custom Filename

Generate dynamic filenames based on date, sheet name, or other criteria:

```tsx
const exportWithTimestamp = () => {
  const timestamp = new Date().toISOString().split('T')[0];
  const filename = `spreadsheet-export-${timestamp}`;
  
  exportRegion?.(range, filename, "image/png");
};

const exportBySheetName = (sheetName: string) => {
  const filename = `${sheetName.toLowerCase().replace(/\s+/g, '-')}-export`;
  
  exportRegion?.(range, filename, "image/png");
};
```

## Exporting Entire Canvas

While there's no direct "export entire canvas" function, you can export the entire visible sheet by specifying the full range:

```tsx
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const { rowCount, columnCount, activeSheetId } = useSpreadsheetState({
  // ... configuration
});

const exportEntireSheet = () => {
  exportRegion?.(
    {
      startRowIndex: 1,
      endRowIndex: rowCount - 1,
      startColumnIndex: 1,
      endColumnIndex: columnCount - 1,
    },
    `sheet-${activeSheetId}-full`,
    "image/png"
  );
};
```

## Complete Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  useSpreadsheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  type SheetData,
} from "@rowsncolumns/spreadsheet-state";

function SpreadsheetWithExport() {
  const [sheets, setSheets] = useState([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sales Data" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    rowCount,
    columnCount,
    getCellData,
    getSheetName,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  return (
    <SpreadsheetProvider>
      <ExportToolbar
        sheetName={getSheetName(activeSheetId)}
        selections={selections}
        activeCell={activeCell}
        rowCount={rowCount}
        columnCount={columnCount}
      />
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
      />
    </SpreadsheetProvider>
  );
}

function ExportToolbar({ sheetName, selections, activeCell, rowCount, columnCount }) {
  const { exportRegion } = useSpreadsheet();

  const exportSelection = () => {
    const range = selections.length > 0
      ? selections[selections.length - 1].range
      : {
          startRowIndex: activeCell.rowIndex,
          endRowIndex: activeCell.rowIndex,
          startColumnIndex: activeCell.columnIndex,
          endColumnIndex: activeCell.columnIndex,
        };

    exportRegion?.(range, `${sheetName}-selection`, "image/png");
  };

  const exportFullSheet = () => {
    exportRegion?.(
      {
        startRowIndex: 1,
        endRowIndex: rowCount - 1,
        startColumnIndex: 1,
        endColumnIndex: columnCount - 1,
      },
      `${sheetName}-full`,
      "image/png"
    );
  };

  return (
    <div className="flex gap-2 p-2">
      <button onClick={exportSelection}>
        Export Selection
      </button>
      <button onClick={exportFullSheet}>
        Export Full Sheet
      </button>
    </div>
  );
}
```

## Use Cases

### Report Generation

Export specific data ranges for inclusion in reports:

```tsx
const exportQuarterlyReport = () => {
  exportRegion?.(
    {
      startRowIndex: 1,
      endRowIndex: 15,
      startColumnIndex: 1,
      endColumnIndex: 8,
    },
    "Q1-2024-report",
    "image/png"
  );
};
```

### Data Visualization Sharing

Export charts and formatted data for sharing:

```tsx
const exportChart = (chartRange) => {
  exportRegion?.(chartRange, "sales-chart", "image/png");
};
```

### Documentation

Create screenshots for documentation or tutorials:

```tsx
const exportExample = () => {
  exportRegion?.(
    exampleRange,
    "documentation-example",
    "image/png"
  );
};
```

## Limitations

* Only the visible/rendered portion of the canvas can be exported
* Hidden rows and columns are not included in the export
* The export captures the current visual state (colors, formatting, etc.)
* Very large ranges may take longer to export
* Export quality depends on the canvas resolution

## Best Practices

1. **Use descriptive filenames**: Include dates, sheet names, or identifiers
2. **Choose appropriate format**: Use PNG for detailed data, JPEG for photographs
3. **Limit range size**: Export only necessary cells for better performance
4. **Provide user feedback**: Show loading states during export
5. **Validate ranges**: Ensure row/column indices are within bounds

## Browser Compatibility

The export functionality works in all modern browsers that support:

* Canvas API
* Blob API
* Download attribute on anchor elements

For older browsers, consider providing a fallback or polyfill.


# Cell format Registry

Spreadsheet lets you store and compress duplicate format data using cellxfs registries

Cell format registry lets you store formats in a central place and reference each format on the cell via a short ID (`sid`) instead of inlining the full format object. This typically gives \~70% compression on cell data.

```tsx
// Before cellXfs registry
// Cell data A1:

{
  uf: {
    backgroundColor: "green",
    horizontalAlignment: "left"
  },
  ue: {
    sv: "Cell with styleId",
  },
  fv: "Cell with styleId",
  note: "Hello world, this is notes.",
},

// After cellXfs registry — `uf` becomes a StyleReference
{
  uf: {
    sid: "123",
  },
  ue: {
    sv: "Cell with styleId",
  },
  fv: "Cell with styleId",
  note: "Hello world, this is notes.",
},
```

{% hint style="info" %}
Only `uf` is written. The effective format is no longer persisted on `CellData` — the renderer derives it on the fly from `uf` (via the registry lookup), the cell value, and (for formula cells) precedent formats.
{% endhint %}

### Usage

```tsx
const App = () => {
  const [cellXfs, onChangeCellXfs] = useState<CellXfs | null | undefined>(
    new Map()
  );
  
  // With useSpreadsheetstate
  const { getEffectiveFormat } = useSpreadsheetState({
     cellXfs,
     onChangeCellXfs
  })
  
  // With yjs
  const { } = useYSpreadsheetV2({
     onChangeCellXfs
  })
  
  return (
      <CanvasGrid getEffectiveFormat={getEffectiveFormat} />
  )
  
}
```


# Shared strings

Spreadsheet can store repeated text in a shared string table and reference it from cell data.

Shared strings let you store repeated text in a central Map and reference it by key from each cell. This reduces duplicated text in `sheetData` and keeps imports/exports consistent with Excel-style shared strings.

When shared strings are enabled, a cell can store an `ss` key and the actual text lives in `sharedStrings`. If `ss` is present, it takes precedence over any string or formatted value on the cell.

```tsx
// Before shared strings
{
  ue: { sv: "Hello" },
  fv: "Hello",
}

// With shared strings (Map-based)
{
  ss: "0"
}
// sharedStrings.get("0") === "Hello"
```

### Usage

```tsx
const App = () => {
  const [sharedStrings, onChangeSharedStrings] = useState<Map<string, string>>(
    new Map(),
  );

  const {
    getFormattedValue,
    getEffectiveExtendedValue,
    getUserEnteredExtendedValue,
  } = useSpreadsheetState({
    sharedStrings,
    onChangeSharedStrings,
  });

  // YJS automatically uses Map-based shared strings
  useYSpreadsheetV2({
    onChangeSharedStrings,
  });

  // For search, so it looks for shared strings index
  useSearch({
    getFormattedValue,
  });

  return (
    <CanvasGrid
      getFormattedValue={getFormattedValue}
      getUserEnteredExtendedValue={getUserEnteredExtendedValue}
      getEffectiveExtendedValue={getEffectiveExtendedValue}
    />
  );
};
```

### Import and export

* Excel/ODS/CSV imports honor `enabledSharedStrings`.
* Exports use `sharedStrings` when `ss` is present (shared strings take precedence over cell-level string/format values).

### Notes

* Shared string entries are not garbage-collected on delete, similar to Excel. Cells simply stop referencing their keys.


# Rich text formatting

Per-segment formatting inside a single cell — bold, italic, underline, strikethrough, color, font family, font size — plus \`@mention\` chips.

Spreadsheet supports per-segment formatting inside a single cell. A run of bold inside an otherwise plain string, a colored phrase in the middle of a sentence, or `@mention` chips inline with text all round-trip through XLSX and render directly on the canvas.

## Storage model

Per-segment formatting is **not** stored on `CellData`. Cells reference a shared-strings entry via `ss`, and rich entries on the SharedStrings side carry `{ text, runs }`. This mirrors XLSX `<si><r>` semantics — one canonical location per rich string, no per-cell duplication.

```typescript
// libs/spreadsheet-state/types.ts
export type RichSharedString = {
  text: string;
  runs: TextFormatRun[];
};

export type SharedStringValue = string | RichSharedString;
export type SharedStrings = Map<string, SharedStringValue>;
```

Two cells with the same rich text + same runs share a single SharedStrings slot. Two cells with the same text but different runs get separate slots (matches Excel — runs are part of the dedup key).

## TextFormatRun

A run is a `[startIndex, endIndex)` slice over the cell's text plus either a `TextFormat` (text run) or a `Mention` (mention chip).

```typescript
// libs/common-types/index.ts
export type TextFormatRun<
  Format extends TextFormat = TextFormat,
  M extends Mention = Mention,
> =
  | {
      startIndex: number;
      endIndex: number;
      format: Format;
      nodeType: "text";
    }
  | {
      startIndex: number;
      endIndex: number;
      nodeType: "mention";
      mention: M;
    };

export type TextFormat = {
  color?: Color | string;
  fontFamily?: string;
  fontSize?: number;
  bold?: boolean;
  italic?: boolean;
  strikethrough?: boolean;
  underline?: boolean;
  vertAlign?: "superscript" | "subscript";
};
```

A cell with `"Hello world"` where `"world"` is bold red would carry:

```typescript
{
  text: "Hello world",
  runs: [
    { startIndex: 0, endIndex: 6, nodeType: "text", format: {} },
    {
      startIndex: 6,
      endIndex: 11,
      nodeType: "text",
      format: { bold: true, color: "#ff0000" },
    },
  ],
}
```

## Rendering rich text

The canvas grid takes a `getTextFormatRuns` prop. When you don't pass it, cells with rich runs render as plain text — the formatting still exists in the data, but the renderer can't see it.

```tsx
import { CanvasGrid } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function MySpreadsheet() {
  const {
    getCellData,
    getTextFormatRuns, // <- exposed by useSpreadsheetState
    // ...other selectors
  } = useSpreadsheetState({
    sheets,
    sheetData,
    sharedStrings,
    onChangeSharedStrings,
    // ...
  });

  return (
    <CanvasGrid
      sheetId={1}
      getCellData={getCellData}
      getTextFormatRuns={getTextFormatRuns}
      // ...
    />
  );
}
```

`getTextFormatRuns(sheetId, rowIndex, columnIndex)` resolves the cell's `ss` key against the `SharedStrings` map and returns the rich runs (or `undefined` for plain cells).

{% hint style="info" %}
Rich-text rendering needs both `sharedStrings` (provided to `useSpreadsheetState`) **and** `getTextFormatRuns` (forwarded to `CanvasGrid`). The runs live on the SharedStrings side, so without shared strings there's nowhere to read them from. See [Shared strings](/configuration/features/shared-strings).
{% endhint %}

## Editing rich text

The default cell editor is a ProseMirror-based rich text editor. Standard shortcuts work out of the box:

| Shortcut               | Effect                      |
| ---------------------- | --------------------------- |
| `Cmd/Ctrl + B`         | Bold the selection          |
| `Cmd/Ctrl + I`         | Italic the selection        |
| `Cmd/Ctrl + U`         | Underline the selection     |
| `Cmd/Ctrl + Shift + X` | Strikethrough the selection |

The toolbar's font color, font size, font family, and decoration buttons also write into the selected range when a cell is open for editing.

When the user commits the edit, the spreadsheet:

1. Builds a `TextFormatRun[]` from the editor state
2. Writes (or reuses) a rich entry in `sharedStrings`
3. Sets `cellData.ss` to point at the slot

`onChange` receives the new value and the runs:

```tsx
onChange(
  sheetId: number,
  cell: CellInterface,
  value: string | boolean,
  textFormatRuns: TextFormatRun[] | null | undefined,
  previousValue?: string | boolean | null,
  isDirty?: boolean,
  previousTextFormatRuns?: TextFormatRun[] | null,
): void
```

`previousTextFormatRuns` lets a host detect a pure-formatting edit (value unchanged, runs differ — for example Cmd+B on existing text) and persist it as a dirty change. `useSpreadsheetState` already handles this.

## XLSX round-trip

Rich text round-trips through XLSX with no extra wiring. On import, `<si><r><rPr/>...<t/></r></si>` blocks parse into `RichSharedString` entries; on export, rich shared strings emit the corresponding `<si><r>` markup.

Supported run properties on the XLSX path:

* `<b/>` — bold
* `<i/>` — italic
* `<u/>` — underline
* `<strike/>` — strikethrough
* `<sz val=N/>` — font size
* `<rFont val="…"/>` — font family
* `<color rgb="…"/>` — ARGB color
* `<color theme="…" tint="…"/>` — theme color

## Caveats

The canvas grid renders runs only when the cell isn't using one of the modes that fundamentally changes how text is painted. Cells in any of these modes fall back to flat-text rendering:

* Chip cells (data validation chips, structured chips)
* `shrinkToFit`
* `textRotation` (any non-horizontal angle, including `"vertical"`)
* `vertAlign` set on the cell-level format (superscript / subscript at the run level is fine)

Mention runs (`nodeType: "mention"`) render as inline chips. See [Mentions](/configuration/features/mentions) for the autocomplete plumbing.


# Mentions

Add @mentions functionality to spreadsheet cells

The mentions feature allows users to reference people, tags, or entities within cells using the `@` symbol, similar to social media platforms. This is useful for collaborative spreadsheets, task management, and commenting systems.

## Overview

Mentions enable:

* **User tagging**: Reference team members in cells
* **Entity references**: Link to external resources or data
* **Autocomplete**: Dropdown suggestions as users type
* **Custom rendering**: Display mentions with custom styling
* **Data binding**: Connect mentions to your application's data model

## Basic Usage

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";
import { useCallback } from "react";

function SpreadsheetWithMentions() {
  const getMentions = useCallback(async (query?: string) => {
    // Fetch mentions from your API or database
    return [
      { label: "John Doe", value: "user-123" },
      { label: "Jane Smith", value: "user-456" },
      { label: "Marketing Team", value: "team-789" },
    ];
  }, []);

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={1}
        getMentions={getMentions}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

## getMentions Function

The `getMentions` callback is called when users type `@` in a cell. It should return a Promise that resolves to an array of mention objects.

### Function Signature

```typescript
type MentionItem = {
  label: string;      // Display text in dropdown
  value: string;      // Unique identifier
  [key: string]: any; // Additional custom properties
};

type GetMentions = (query?: string) => Promise<MentionItem[]>;
```

### Parameters

* **query** (optional): The search text entered by the user after `@`

### Example with Search

```tsx
const getMentions = useCallback(async (query?: string) => {
  // Filter mentions based on query
  const allMentions = [
    { label: "Alice Johnson", value: "user-1", email: "alice@example.com" },
    { label: "Bob Wilson", value: "user-2", email: "bob@example.com" },
    { label: "Charlie Brown", value: "user-3", email: "charlie@example.com" },
  ];

  if (!query) {
    return allMentions;
  }

  // Filter by query
  const filtered = allMentions.filter((mention) =>
    mention.label.toLowerCase().includes(query.toLowerCase())
  );

  return filtered;
}, []);
```

## Custom Dropdown Rendering

Customize how mentions appear in the autocomplete dropdown using `MentionDropdownItemComponent`:

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";

function SpreadsheetWithCustomMentions() {
  const MentionItem = ({ mention }) => {
    return (
      <div className="flex items-center gap-2 p-2">
        <img
          src={mention.avatar}
          alt={mention.label}
          className="w-8 h-8 rounded-full"
        />
        <div>
          <div className="font-semibold">{mention.label}</div>
          <div className="text-xs text-gray-500">{mention.email}</div>
        </div>
      </div>
    );
  };

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={1}
        getMentions={getMentions}
        MentionDropdownItemComponent={MentionItem}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

### MentionDropdownItemComponent Props

```typescript
type MentionDropdownItemProps = {
  mention: MentionItem;
  isSelected?: boolean;
  onClick?: () => void;
};
```

## Async Data Loading

Fetch mentions from an API:

```tsx
const getMentions = useCallback(async (query?: string) => {
  try {
    const response = await fetch(
      `/api/mentions?query=${encodeURIComponent(query || '')}`
    );
    const data = await response.json();
    return data.mentions;
  } catch (error) {
    console.error("Failed to fetch mentions:", error);
    return [];
  }
}, []);
```

## Complete Example

```tsx
import React, { useCallback, useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
} from "@rowsncolumns/spreadsheet-state";

// Mock user database
const users = [
  {
    label: "Alice Johnson",
    value: "user-1",
    email: "alice@company.com",
    department: "Engineering",
    avatar: "/avatars/alice.jpg",
  },
  {
    label: "Bob Smith",
    value: "user-2",
    email: "bob@company.com",
    department: "Marketing",
    avatar: "/avatars/bob.jpg",
  },
  {
    label: "Charlie Davis",
    value: "user-3",
    email: "charlie@company.com",
    department: "Sales",
    avatar: "/avatars/charlie.jpg",
  },
];

function TaskSpreadsheet() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 10, title: "Tasks" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
    onChange,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  // Fetch mentions with search
  const getMentions = useCallback(async (query?: string) => {
    // Simulate API delay
    await new Promise((resolve) => setTimeout(resolve, 100));

    if (!query) {
      return users;
    }

    // Filter by name or email
    return users.filter((user) =>
      user.label.toLowerCase().includes(query.toLowerCase()) ||
      user.email.toLowerCase().includes(query.toLowerCase()) ||
      user.department.toLowerCase().includes(query.toLowerCase())
    );
  }, []);

  // Custom mention dropdown item
  const MentionDropdownItem = ({ mention }) => {
    return (
      <div className="flex items-center gap-3 px-3 py-2 hover:bg-gray-100">
        <img
          src={mention.avatar}
          alt={mention.label}
          className="w-10 h-10 rounded-full"
        />
        <div className="flex-1">
          <div className="font-medium text-sm">{mention.label}</div>
          <div className="text-xs text-gray-600">{mention.email}</div>
        </div>
        <span className="text-xs text-gray-500 bg-gray-200 px-2 py-1 rounded">
          {mention.department}
        </span>
      </div>
    );
  };

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
        onChangeActiveCell={onChangeActiveCell}
        onChangeSelections={onChangeSelections}
        onChange={onChange}
        getMentions={getMentions}
        MentionDropdownItemComponent={MentionDropdownItem}
      />
    </SpreadsheetProvider>
  );
}

export default TaskSpreadsheet;
```

## Accessing Mention Data

When a mention is selected, it's stored in the cell data. You can access it through the cell's data structure:

```tsx
const cellData = getCellData(sheetId, rowIndex, columnIndex);

// Check if cell contains mentions
if (cellData?.mentions) {
  cellData.mentions.forEach((mention) => {
    console.log("Mentioned user:", mention.label, mention.value);
  });
}
```

## Use Cases

### Team Collaboration

```tsx
// Track task assignments
const getMentions = async () => [
  { label: "@alice", value: "user-1", role: "Developer" },
  { label: "@bob", value: "user-2", role: "Designer" },
];
```

### Project Management

```tsx
// Reference project resources
const getMentions = async (query) => {
  return [
    { label: "@project-alpha", value: "proj-1", type: "project" },
    { label: "@milestone-q1", value: "mile-1", type: "milestone" },
    { label: "@alice", value: "user-1", type: "user" },
  ];
};
```

### Comments and Notes

```tsx
// Add comments with user mentions
const getMentions = async () => {
  const teamMembers = await fetchTeamMembers();
  return teamMembers.map((member) => ({
    label: `@${member.username}`,
    value: member.id,
    name: member.fullName,
  }));
};
```

## Styling

Mentions in cells can be styled using custom text format runs. The spreadsheet automatically formats mentioned text when rendered.

## Performance Optimization

### Debounced Search

```tsx
import { useMemo } from "react";
import debounce from "lodash/debounce";

function SpreadsheetWithMentions() {
  const fetchMentions = async (query?: string) => {
    const response = await fetch(`/api/mentions?q=${query}`);
    return response.json();
  };

  const getMentions = useMemo(
    () => debounce(fetchMentions, 300),
    []
  );

  return (
    <CanvasGrid
      getMentions={getMentions}
      // ... other props
    />
  );
}
```

### Caching

```tsx
const mentionCache = new Map();

const getMentions = useCallback(async (query?: string) => {
  const cacheKey = query || "all";

  if (mentionCache.has(cacheKey)) {
    return mentionCache.get(cacheKey);
  }

  const mentions = await fetchMentions(query);
  mentionCache.set(cacheKey, mentions);

  return mentions;
}, []);
```

## Best Practices

1. **Limit Results**: Return a maximum of 10-20 mentions to keep the dropdown manageable
2. **Fast Response**: Keep the `getMentions` function fast (< 300ms) for good UX
3. **Error Handling**: Handle API failures gracefully and return an empty array
4. **Unique Values**: Ensure each mention has a unique `value` property
5. **Clear Labels**: Use descriptive labels that help users identify the right mention
6. **Type Safety**: Use TypeScript to define your mention structure

## Troubleshooting

### Mentions Not Appearing

* Verify `getMentions` returns an array of objects with `label` and `value` properties
* Check that the function returns a Promise
* Ensure there are no JavaScript errors in the console

### Search Not Working

* Confirm the `query` parameter is being used in your filter logic
* Test the search function independently
* Check for case sensitivity issues

### Slow Performance

* Implement debouncing for API calls
* Add caching for frequently accessed results
* Limit the number of returned results
* Use pagination for large datasets


# Cell Tooltips and Popovers

Display tooltips and expandable content for spreadsheet cells

Enhance your spreadsheet with custom tooltips and expandable cell content. Display additional information, rich media, or interactive components when users hover over or click on cells.

## Overview

The spreadsheet provides two main ways to display additional cell content:

* **Tooltips** (`getTooltipContent`): Show hover tooltips with custom content
* **Expandable Content** (`getCellExpandContent`): Display rich content in a popover when cells are expanded

## Cell Tooltips

Display custom tooltips when users hover over cells using the `getTooltipContent` callback.

### Basic Usage

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";

function SpreadsheetWithTooltips() {
  const getTooltipContent = (
    sheetId: number,
    rowIndex: number,
    columnIndex: number
  ) => {
    // Return undefined for no tooltip
    if (rowIndex === 0 || columnIndex === 0) {
      return undefined;
    }

    // Return JSX for custom tooltip
    return (
      <div className="p-2">
        <strong>Cell:</strong> {cellToAddress({ rowIndex, columnIndex })}
        <br />
        <strong>Sheet:</strong> {sheetId}
      </div>
    );
  };

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={1}
        getTooltipContent={getTooltipContent}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

### Function Signature

```typescript
type GetTooltipContent = (
  sheetId: number,
  rowIndex: number,
  columnIndex: number
) => React.ReactNode | undefined;
```

### Return Values

* **React.ReactNode**: Display tooltip with custom content
* **undefined**: No tooltip for this cell

### Examples

#### Display Cell Metadata

```tsx
const getTooltipContent = (sheetId, rowIndex, columnIndex) => {
  const cellData = getCellData(sheetId, rowIndex, columnIndex);

  if (!cellData) return undefined;

  return (
    <div className="text-sm">
      <div className="font-semibold mb-1">Cell Information</div>
      {cellData.note && (
        <div className="mb-1">
          <strong>Note:</strong> {cellData.note}
        </div>
      )}
      {cellData.ue?.fv && (
        <div>
          <strong>Formula:</strong> {cellData.ue.fv}
        </div>
      )}
    </div>
  );
};
```

#### Show Validation Rules

```tsx
const getTooltipContent = (sheetId, rowIndex, columnIndex) => {
  const validation = getDataValidation(sheetId, rowIndex, columnIndex);

  if (!validation) return undefined;

  return (
    <div className="p-2 bg-yellow-50 border border-yellow-200 rounded">
      <div className="font-semibold text-yellow-800">Validation Rule</div>
      <div className="text-sm text-yellow-700 mt-1">
        {validation.condition?.type}: {validation.condition?.values?.join(", ")}
      </div>
    </div>
  );
};
```

#### Display Error Messages

```tsx
const getTooltipContent = (sheetId, rowIndex, columnIndex) => {
  const cellData = getCellData(sheetId, rowIndex, columnIndex);

  if (cellData?.ev?.ev) {
    return (
      <div className="p-2 bg-red-50 border border-red-200 rounded">
        <div className="font-semibold text-red-800">Formula Error</div>
        <div className="text-sm text-red-700 mt-1">
          {cellData.ev.ev.name ?? cellData.ev.ev.message}
        </div>
      </div>
    );
  }

  return undefined;
};
```

## Expandable Cell Content

Display rich, interactive content when users expand cells using the `getCellExpandContent` callback.

### Basic Usage

```tsx
function SpreadsheetWithExpandableContent() {
  const getCellExpandContent = () => {
    return (
      <div className="p-4 max-w-md">
        <h3 className="font-bold mb-2">Additional Information</h3>
        <p className="text-sm text-gray-700">
          This is expandable content that appears when the cell is expanded.
          You can include any React component here.
        </p>
      </div>
    );
  };

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={1}
        getCellExpandContent={getCellExpandContent}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

### Function Signature

```typescript
type GetCellExpandContent = () => React.ReactNode;
```

### Advanced Examples

#### Rich Text Editor

```tsx
const getCellExpandContent = () => {
  return (
    <div className="overflow-auto max-h-96 max-w-2xl p-4">
      <div className="prose">
        <h3>Project Description</h3>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.
          Detailed information about this cell can be displayed here.
        </p>
        <ul>
          <li>Feature A</li>
          <li>Feature B</li>
          <li>Feature C</li>
        </ul>
      </div>
    </div>
  );
};
```

#### Image Gallery

```tsx
const getCellExpandContent = () => {
  const images = [
    "/images/chart1.png",
    "/images/chart2.png",
    "/images/chart3.png",
  ];

  return (
    <div className="p-4 max-w-4xl">
      <h3 className="font-bold mb-4">Related Charts</h3>
      <div className="grid grid-cols-3 gap-4">
        {images.map((src, index) => (
          <img
            key={index}
            src={src}
            alt={`Chart ${index + 1}`}
            className="rounded shadow-lg"
          />
        ))}
      </div>
    </div>
  );
};
```

#### Interactive Form

```tsx
const getCellExpandContent = () => {
  const [notes, setNotes] = useState("");

  return (
    <div className="p-4 max-w-md">
      <h3 className="font-bold mb-2">Add Notes</h3>
      <textarea
        className="w-full border rounded p-2 mb-2"
        rows={4}
        value={notes}
        onChange={(e) => setNotes(e.target.value)}
        placeholder="Enter your notes here..."
      />
      <button
        className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
        onClick={() => console.log("Save notes:", notes)}
      >
        Save
      </button>
    </div>
  );
};
```

#### Data Visualization

```tsx
import { Chart } from "react-chartjs-2";

const getCellExpandContent = () => {
  const chartData = {
    labels: ["Jan", "Feb", "Mar", "Apr", "May"],
    datasets: [
      {
        label: "Sales",
        data: [12, 19, 3, 5, 2],
        backgroundColor: "rgba(75, 192, 192, 0.2)",
        borderColor: "rgba(75, 192, 192, 1)",
      },
    ],
  };

  return (
    <div className="p-4 max-w-2xl">
      <h3 className="font-bold mb-4">Sales Trend</h3>
      <Chart type="line" data={chartData} />
    </div>
  );
};
```

## Cell-Specific Content

Customize content based on the cell being expanded:

```tsx
import { useSpreadsheet } from "@rowsncolumns/spreadsheet";

function SpreadsheetWithDynamicContent() {
  const { activeCell } = useSpreadsheet();

  const getCellExpandContent = useCallback(() => {
    const cellData = getCellData(
      activeSheetId,
      activeCell.rowIndex,
      activeCell.columnIndex
    );

    // Different content based on cell value
    if (cellData?.fv?.includes("Task")) {
      return <TaskDetailsPanel cellData={cellData} />;
    }

    if (cellData?.fv?.includes("User")) {
      return <UserProfilePanel cellData={cellData} />;
    }

    // Default content
    return (
      <div className="p-4">
        <p>No additional information available</p>
      </div>
    );
  }, [activeCell, activeSheetId]);

  return (
    <CanvasGrid
      getCellExpandContent={getCellExpandContent}
      // ... other props
    />
  );
}
```

## Complete Example

```tsx
import React, { useState, useCallback } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
} from "@rowsncolumns/spreadsheet-state";
import { cellToAddress } from "@rowsncolumns/utils";

function EnhancedSpreadsheet() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Data" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  // Tooltip handler
  const getTooltipContent = useCallback(
    (sheetId: number, rowIndex: number, columnIndex: number) => {
      // Skip headers
      if (rowIndex === 0 || columnIndex === 0) return undefined;

      const cellData = getCellData(sheetId, rowIndex, columnIndex);

      if (!cellData) return undefined;

      const address = cellToAddress({ rowIndex, columnIndex });

      return (
        <div className="p-3 bg-white shadow-lg rounded-lg border">
          <div className="text-xs text-gray-500 mb-1">{address}</div>
          {cellData.note && (
            <div className="text-sm border-t pt-2 mt-2">
              <strong className="text-gray-700">Note:</strong>
              <div className="text-gray-600 mt-1">{cellData.note}</div>
            </div>
          )}
          {cellData.hyperlink && (
            <div className="text-sm border-t pt-2 mt-2">
              <strong className="text-blue-700">Link:</strong>
              <a
                href={cellData.hyperlink}
                className="text-blue-600 hover:underline ml-1"
              >
                {cellData.hyperlink}
              </a>
            </div>
          )}
        </div>
      );
    },
    [getCellData]
  );

  // Expandable content handler
  const getCellExpandContent = useCallback(() => {
    const cellData = getCellData(
      activeSheetId,
      activeCell.rowIndex,
      activeCell.columnIndex
    );

    return (
      <div className="overflow-auto max-h-96 max-w-2xl p-6 bg-white rounded-lg">
        <h2 className="text-xl font-bold mb-4">Cell Details</h2>

        <div className="space-y-4">
          <div>
            <strong className="text-gray-700">Location:</strong>
            <span className="ml-2">
              {cellToAddress({
                rowIndex: activeCell.rowIndex,
                columnIndex: activeCell.columnIndex,
              })}
            </span>
          </div>

          {cellData?.fv && (
            <div>
              <strong className="text-gray-700">Value:</strong>
              <div className="mt-1 p-3 bg-gray-50 rounded font-mono text-sm">
                {cellData.fv}
              </div>
            </div>
          )}

          {cellData?.ue?.fv && (
            <div>
              <strong className="text-gray-700">Formula:</strong>
              <div className="mt-1 p-3 bg-blue-50 rounded font-mono text-sm">
                {cellData.ue.fv}
              </div>
            </div>
          )}

          <div className="border-t pt-4">
            <button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
              Edit Cell
            </button>
          </div>
        </div>
      </div>
    );
  }, [activeCell, activeSheetId, getCellData]);

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
        onChangeActiveCell={onChangeActiveCell}
        onChangeSelections={onChangeSelections}
        getTooltipContent={getTooltipContent}
        getCellExpandContent={getCellExpandContent}
      />
    </SpreadsheetProvider>
  );
}

export default EnhancedSpreadsheet;
```

## Styling

### Tooltip Styling

Tooltips automatically position themselves to avoid going off-screen. Style them using standard CSS classes:

```tsx
const getTooltipContent = () => (
  <div className="p-3 bg-gray-900 text-white rounded-lg shadow-xl max-w-xs">
    <div className="text-sm">Your tooltip content</div>
  </div>
);
```

### Popover Styling

Expandable content appears in a popover with scrolling support:

```tsx
const getCellExpandContent = () => (
  <div className="overflow-auto max-h-96 max-w-4xl p-6">
    {/* Scrollable content */}
  </div>
);
```

## Use Cases

### Data Annotations

Show additional context or metadata for cells containing important data.

### Error Explanations

Display helpful error messages and suggestions when formulas fail.

### Rich Content Preview

Preview images, charts, or formatted text without cluttering the grid.

### Interactive Dialogs

Provide forms or interactive elements for complex data entry.

### Audit Information

Display who last edited a cell and when.

## Performance Considerations

1. **Memoize Callbacks**: Use `useCallback` to prevent unnecessary re-renders
2. **Conditional Rendering**: Return `undefined` when no tooltip is needed
3. **Lazy Loading**: Load heavy content only when the popover is opened
4. **Limit Complexity**: Keep tooltip content simple for smooth hover interactions

## Best Practices

1. **Keep Tooltips Concise**: Display only essential information in tooltips
2. **Use Popovers for Rich Content**: Save complex content for expandable popovers
3. **Avoid Heavy Computations**: Don't perform expensive calculations in tooltip callbacks
4. **Accessibility**: Ensure tooltips and popovers work with keyboard navigation
5. **Consistent Styling**: Match your application's design system

## Troubleshooting

### Tooltips Not Showing

* Verify `getTooltipContent` returns valid JSX or undefined (not null)
* Check that the function is properly passed to CanvasGrid
* Ensure there are no JavaScript errors in the console

### Popovers Not Opening

* Confirm `getCellExpandContent` is defined
* Check that the cell expansion trigger is working (usually double-click or icon)
* Verify the content isn't too large or causing layout issues

### Performance Issues

* Use `React.memo` for complex tooltip components
* Implement lazy loading for heavy content
* Consider debouncing tooltip display


# Multi-Instance Spreadsheets

Run multiple spreadsheet instances in the same application

The `SpreadsheetMultiInstanceProvider` component allows you to render multiple spreadsheet instances within the same application while maintaining isolated state and preventing conflicts between instances.

## Overview

By default, each spreadsheet instance shares global context through `SpreadsheetProvider`. When you need to render multiple spreadsheets simultaneously (e.g., side-by-side comparison, multi-document interface), you need to wrap them in `SpreadsheetMultiInstanceProvider` to ensure proper isolation.

## Basic Usage

```tsx
import {
  SpreadsheetProvider,
  SpreadsheetMultiInstanceProvider,
  CanvasGrid,
} from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function MultiSpreadsheetApp() {
  return (
    <SpreadsheetMultiInstanceProvider>
      <SpreadsheetProvider>
        <SpreadsheetInstance instanceId="spreadsheet-1" />
      </SpreadsheetProvider>

      <SpreadsheetProvider>
        <SpreadsheetInstance instanceId="spreadsheet-2" />
      </SpreadsheetProvider>
    </SpreadsheetMultiInstanceProvider>
  );
}

function SpreadsheetInstance({ instanceId }: { instanceId: string }) {
  const {
    activeCell,
    activeSheetId,
    selections,
    // ... other state
  } = useSpreadsheetState({
    // configuration
  });

  return (
    <CanvasGrid
      instanceId={instanceId}
      sheetId={activeSheetId}
      activeCell={activeCell}
      selections={selections}
      // ... other props
    />
  );
}
```

## Instance ID

Each `CanvasGrid` instance should have a unique `instanceId` prop when rendered within `SpreadsheetMultiInstanceProvider`:

```tsx
<CanvasGrid
  instanceId="spreadsheet-a"  // Unique identifier
  sheetId={activeSheetId}
  // ... other props
/>
```

### Why Instance IDs Matter

Instance IDs ensure that:

* Keyboard events are routed to the correct spreadsheet
* Editor state is isolated between instances
* Context menus and dialogs appear in the correct location
* Formula evaluation doesn't cross instance boundaries

## Use Cases

### Side-by-Side Comparison

Compare two spreadsheets side by side:

```tsx
function SpreadsheetComparison() {
  return (
    <SpreadsheetMultiInstanceProvider>
      <div className="flex gap-4">
        <div className="flex-1">
          <h2>Version 1</h2>
          <SpreadsheetProvider>
            <SpreadsheetView instanceId="version-1" />
          </SpreadsheetProvider>
        </div>

        <div className="flex-1">
          <h2>Version 2</h2>
          <SpreadsheetProvider>
            <SpreadsheetView instanceId="version-2" />
          </SpreadsheetProvider>
        </div>
      </div>
    </SpreadsheetMultiInstanceProvider>
  );
}
```

### Multi-Document Interface

Create a tabbed or windowed interface with multiple spreadsheet documents:

```tsx
function MultiDocumentInterface() {
  const [documents] = useState([
    { id: "doc-1", name: "Budget 2024" },
    { id: "doc-2", name: "Sales Report" },
    { id: "doc-3", name: "Inventory" },
  ]);

  return (
    <SpreadsheetMultiInstanceProvider>
      {documents.map((doc) => (
        <SpreadsheetProvider key={doc.id}>
          <div className="document-window">
            <h3>{doc.name}</h3>
            <SpreadsheetView instanceId={doc.id} />
          </div>
        </SpreadsheetProvider>
      ))}
    </SpreadsheetMultiInstanceProvider>
  );
}
```

### Master-Detail View

Display a master spreadsheet with a detail view:

```tsx
function MasterDetailView() {
  const [selectedRow, setSelectedRow] = useState<number | null>(null);

  return (
    <SpreadsheetMultiInstanceProvider>
      <div className="grid grid-cols-2 gap-4">
        {/* Master spreadsheet */}
        <SpreadsheetProvider>
          <h2>Sales Overview</h2>
          <SpreadsheetView
            instanceId="master"
            onRowSelect={setSelectedRow}
          />
        </SpreadsheetProvider>

        {/* Detail spreadsheet */}
        {selectedRow && (
          <SpreadsheetProvider>
            <h2>Row Details</h2>
            <SpreadsheetView
              instanceId="detail"
              rowData={selectedRow}
            />
          </SpreadsheetProvider>
        )}
      </div>
    </SpreadsheetMultiInstanceProvider>
  );
}
```

## Complete Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  SpreadsheetMultiInstanceProvider,
  CanvasGrid,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
} from "@rowsncolumns/spreadsheet-state";

function App() {
  return (
    <SpreadsheetMultiInstanceProvider>
      <div className="flex gap-5 h-screen p-5">
        <div className="flex-1">
          <SpreadsheetProvider>
            <SpreadsheetA />
          </SpreadsheetProvider>
        </div>

        <div className="flex-1">
          <SpreadsheetProvider>
            <SpreadsheetB />
          </SpreadsheetProvider>
        </div>
      </div>
    </SpreadsheetMultiInstanceProvider>
  );
}

function SpreadsheetA() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet A" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
    onChange,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  return (
    <CanvasGrid
      instanceId="spreadsheet-a"
      sheetId={activeSheetId}
      activeCell={activeCell}
      selections={selections}
      getCellData={getCellData}
      onChangeActiveCell={onChangeActiveCell}
      onChangeSelections={onChangeSelections}
      onChange={onChange}
    />
  );
}

function SpreadsheetB() {
  // Similar implementation with different instance ID
  // ...

  return (
    <CanvasGrid
      instanceId="spreadsheet-b"
      // ... props
    />
  );
}
```

## Best Practices

1. **Always Use Unique Instance IDs**: Ensure each spreadsheet has a unique `instanceId` to prevent conflicts
2. **Wrap Each Instance**: Each spreadsheet should have its own `SpreadsheetProvider` wrapper
3. **Isolate State**: Use separate state management for each spreadsheet instance
4. **Performance Considerations**: Be mindful of rendering multiple large spreadsheets simultaneously - consider lazy loading or virtualization
5. **Memory Management**: Clean up instances when they're no longer needed to free up resources

## Limitations

* Each instance maintains its own calculation engine and state
* Cross-instance formulas are not supported (formulas cannot reference cells from other instances)
* Each instance requires separate data management

## Performance Tips

When rendering multiple instances:

```tsx
// Use React.memo to prevent unnecessary re-renders
const SpreadsheetInstance = React.memo(({ instanceId, data }) => {
  // ... implementation
});

// Lazy load instances that aren't immediately visible
const LazySpreadsheet = lazy(() => import('./SpreadsheetInstance'));

function MultiInstance() {
  return (
    <SpreadsheetMultiInstanceProvider>
      <Suspense fallback={<div>Loading...</div>}>
        <LazySpreadsheet instanceId="lazy-1" />
      </Suspense>
    </SpreadsheetMultiInstanceProvider>
  );
}
```

## Troubleshooting

### Keyboard Events Not Working

If keyboard events aren't working properly, ensure:

* Each `CanvasGrid` has a unique `instanceId`
* The `SpreadsheetMultiInstanceProvider` wraps all instances
* Only one spreadsheet has focus at a time

### State Conflicts

If you experience state conflicts between instances:

* Verify each instance has its own `SpreadsheetProvider`
* Check that state management is properly isolated
* Ensure instance IDs are unique and don't change during component lifecycle


# User-Defined Colors

Create custom color palettes for your spreadsheet

User-defined colors allow you to extend the default color palette with custom colors that persist across your application. Users can add their own colors to color pickers for text, backgrounds, and borders.

## Overview

By default, color selectors in the toolbar (background color, text color, border color) provide a standard palette. User-defined colors extend this palette with:

* **Custom brand colors**: Add your organization's brand colors
* **Project-specific palettes**: Create color schemes for different projects
* **User preferences**: Allow users to save frequently used colors
* **Persistent colors**: Colors that persist across sessions

To replace the picker UI itself rather than extend its palette, see [Custom Color Picker](/configuration/features/custom-color-picker).

## Basic Usage

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Toolbar,
  BackgroundColorSelector,
  TextColorSelector,
  BorderSelector,
} from "@rowsncolumns/spreadsheet";

function SpreadsheetWithCustomColors() {
  const [userDefinedColors, setUserDefinedColors] = useState<string[]>([
    "#FF6B6B", // Coral Red
    "#4ECDC4", // Turquoise
    "#45B7D1", // Sky Blue
    "#FFA07A", // Light Salmon
  ]);

  const handleAddColor = (color: string) => {
    setUserDefinedColors((prev) => [...prev, color]);
  };

  return (
    <SpreadsheetProvider>
      <Toolbar>
        <BackgroundColorSelector
          color={currentCellFormat?.backgroundColor}
          theme={theme}
          userDefinedColors={userDefinedColors}
          onAddUserDefinedColor={handleAddColor}
          onChange={onChangeBackgroundColor}
        />

        <TextColorSelector
          color={currentCellFormat?.textFormat?.color}
          theme={theme}
          isDarkMode={isDarkMode}
          userDefinedColors={userDefinedColors}
          onAddUserDefinedColor={handleAddColor}
          onChange={onChangeTextColor}
        />

        <BorderSelector
          borders={currentCellFormat?.borders}
          theme={theme}
          isDarkMode={isDarkMode}
          userDefinedColors={userDefinedColors}
          onAddUserDefinedColor={handleAddColor}
          onChange={onChangeBorder}
        />
      </Toolbar>

      <CanvasGrid
        // ... props
      />
    </SpreadsheetProvider>
  );
}
```

## State Management

### Simple State

Use React state for basic color management:

```tsx
function MySpreadsheet() {
  const [userDefinedColors, setUserDefinedColors] = useState<string[]>([]);

  const addColor = (color: string) => {
    setUserDefinedColors((prev) => [...prev, color]);
  };

  return (
    <BackgroundColorSelector
      userDefinedColors={userDefinedColors}
      onAddUserDefinedColor={addColor}
      // ... other props
    />
  );
}
```

### Persistent Storage

Save colors to localStorage for persistence:

```tsx
function MySpreadsheet() {
  const [userDefinedColors, setUserDefinedColors] = useState<string[]>(() => {
    // Load from localStorage on mount
    const saved = localStorage.getItem("spreadsheet-custom-colors");
    return saved ? JSON.parse(saved) : [];
  });

  const addColor = (color: string) => {
    setUserDefinedColors((prev) => {
      const updated = [...prev, color];
      // Save to localStorage
      localStorage.setItem(
        "spreadsheet-custom-colors",
        JSON.stringify(updated)
      );
      return updated;
    });
  };

  return (
    <BackgroundColorSelector
      userDefinedColors={userDefinedColors}
      onAddUserDefinedColor={addColor}
      // ... other props
    />
  );
}
```

### Server-Side Persistence

Save colors to your backend:

```tsx
function MySpreadsheet() {
  const [userDefinedColors, setUserDefinedColors] = useState<string[]>([]);

  // Load colors from server
  useEffect(() => {
    async function loadColors() {
      const response = await fetch("/api/user/colors");
      const colors = await response.json();
      setUserDefinedColors(colors);
    }
    loadColors();
  }, []);

  const addColor = async (color: string) => {
    // Optimistically update UI
    setUserDefinedColors((prev) => [...prev, color]);

    // Save to server
    try {
      await fetch("/api/user/colors", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ color }),
      });
    } catch (error) {
      console.error("Failed to save color:", error);
      // Revert on error
      setUserDefinedColors((prev) => prev.filter((c) => c !== color));
    }
  };

  return (
    <BackgroundColorSelector
      userDefinedColors={userDefinedColors}
      onAddUserDefinedColor={addColor}
      // ... other props
    />
  );
}
```

## Color Format

User-defined colors support various formats:

```tsx
const [userDefinedColors, setUserDefinedColors] = useState([
  "#FF6B6B",           // Hex
  "rgb(78, 205, 196)", // RGB
  "rgba(78, 205, 196, 0.8)", // RGBA
  "hsl(180, 60%, 55%)", // HSL
]);
```

## Complete Example

```tsx
import React, { useState, useEffect } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Toolbar,
  BackgroundColorSelector,
  TextColorSelector,
  BorderSelector,
  Sheet,
  SpreadsheetTheme,
  defaultSpreadsheetTheme,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
} from "@rowsncolumns/spreadsheet-state";

// Default custom colors
const DEFAULT_CUSTOM_COLORS = [
  "#FF6B6B", // Coral Red
  "#4ECDC4", // Turquoise
  "#45B7D1", // Sky Blue
  "#FFA07A", // Light Salmon
  "#96CEB4", // Sage Green
  "#FFEAA7", // Warm Yellow
  "#DFE6E9", // Light Gray
  "#74B9FF", // Soft Blue
];

function SpreadsheetWithColors() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet 1" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});
  const [theme] = useState<SpreadsheetTheme>(defaultSpreadsheetTheme);

  // Load user-defined colors from localStorage
  const [userDefinedColors, setUserDefinedColors] = useState<string[]>(() => {
    const saved = localStorage.getItem("user-colors");
    return saved ? JSON.parse(saved) : DEFAULT_CUSTOM_COLORS;
  });

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    getEffectiveFormat,
    onChangeActiveCell,
    onChangeSelections,
    onChange,
    onChangeFormatting,
    onChangeBorder,
    isDarkMode,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  // Get current cell format
  const currentCellFormat = getEffectiveFormat(
    activeSheetId,
    activeCell.rowIndex,
    activeCell.columnIndex
  );

  // Handle adding new color
  const handleAddColor = (color: string) => {
    setUserDefinedColors((prev) => {
      // Avoid duplicates
      if (prev.includes(color)) return prev;

      const updated = [...prev, color];

      // Save to localStorage
      localStorage.setItem("user-colors", JSON.stringify(updated));

      return updated;
    });
  };

  // Handle background color change
  const handleBackgroundColorChange = (color: any) => {
    onChangeFormatting(
      activeSheetId,
      activeCell,
      selections,
      "backgroundColor",
      color
    );
  };

  // Handle text color change
  const handleTextColorChange = (color: any) => {
    onChangeFormatting(
      activeSheetId,
      activeCell,
      selections,
      "textFormat",
      { color }
    );
  };

  return (
    <SpreadsheetProvider>
      <div className="flex flex-col h-screen">
        <Toolbar>
          <BackgroundColorSelector
            color={currentCellFormat?.backgroundColor}
            theme={theme}
            userDefinedColors={userDefinedColors}
            onAddUserDefinedColor={handleAddColor}
            onChange={handleBackgroundColorChange}
          />

          <TextColorSelector
            color={currentCellFormat?.textFormat?.color}
            theme={theme}
            isDarkMode={isDarkMode}
            userDefinedColors={userDefinedColors}
            onAddUserDefinedColor={handleAddColor}
            onChange={handleTextColorChange}
          />

          <BorderSelector
            borders={currentCellFormat?.borders}
            theme={theme}
            isDarkMode={isDarkMode}
            userDefinedColors={userDefinedColors}
            onAddUserDefinedColor={handleAddColor}
            onChange={onChangeBorder}
          />
        </Toolbar>

        <div className="flex-1">
          <CanvasGrid
            sheetId={activeSheetId}
            activeCell={activeCell}
            selections={selections}
            getCellData={getCellData}
            onChangeActiveCell={onChangeActiveCell}
            onChangeSelections={onChangeSelections}
            onChange={onChange}
            theme={theme}
          />
        </div>
      </div>
    </SpreadsheetProvider>
  );
}

export default SpreadsheetWithColors;
```

## Advanced Features

### Color Validation

Validate colors before adding them:

```tsx
function isValidColor(color: string): boolean {
  const style = new Option().style;
  style.color = color;
  return style.color !== "";
}

const addColor = (color: string) => {
  if (!isValidColor(color)) {
    console.error("Invalid color:", color);
    return;
  }

  setUserDefinedColors((prev) => [...prev, color]);
};
```

### Color Limits

Limit the number of custom colors:

```tsx
const MAX_CUSTOM_COLORS = 20;

const addColor = (color: string) => {
  setUserDefinedColors((prev) => {
    if (prev.length >= MAX_CUSTOM_COLORS) {
      console.warn("Maximum custom colors reached");
      return prev;
    }
    return [...prev, color];
  });
};
```

### Color Organization

Organize colors by category:

```tsx
const [colorCategories, setColorCategories] = useState({
  brand: ["#FF6B6B", "#4ECDC4"],
  pastels: ["#FFA07A", "#96CEB4"],
  vibrant: ["#45B7D1", "#FFEAA7"],
});

// Flatten for color selectors
const allColors = Object.values(colorCategories).flat();
```

### Remove Colors

Allow users to remove custom colors:

```tsx
function ColorManager() {
  const [userDefinedColors, setUserDefinedColors] = useState<string[]>([]);

  const removeColor = (colorToRemove: string) => {
    setUserDefinedColors((prev) =>
      prev.filter((color) => color !== colorToRemove)
    );
  };

  return (
    <div className="space-y-2">
      {userDefinedColors.map((color) => (
        <div key={color} className="flex items-center gap-2">
          <div
            className="w-8 h-8 rounded border"
            style={{ backgroundColor: color }}
          />
          <span className="flex-1">{color}</span>
          <button
            onClick={() => removeColor(color)}
            className="px-2 py-1 text-sm text-red-600"
          >
            Remove
          </button>
        </div>
      ))}
    </div>
  );
}
```

## Integration with Theme Colors

User-defined colors work alongside theme colors:

```tsx
import { SpreadsheetTheme } from "@rowsncolumns/spreadsheet";

const customTheme: SpreadsheetTheme = {
  ...defaultSpreadsheetTheme,
  colors: [
    "#000000", // Black
    "#FFFFFF", // White
    "#FF0000", // Red
    // ... theme colors
  ],
};

// User-defined colors appear separately from theme colors
const [userDefinedColors, setUserDefinedColors] = useState([
  "#FF6B6B",
  "#4ECDC4",
]);
```

## Use Cases

### Brand Colors

```tsx
const brandColors = [
  "#1E40AF", // Primary Blue
  "#F59E0B", // Accent Orange
  "#10B981", // Success Green
  "#EF4444", // Error Red
];

const [userDefinedColors, setUserDefinedColors] = useState(brandColors);
```

### Project-Specific Palettes

```tsx
const projectPalettes = {
  marketing: ["#FF6B6B", "#4ECDC4", "#FFA07A"],
  finance: ["#1E40AF", "#10B981", "#F59E0B"],
  design: ["#8B5CF6", "#EC4899", "#F59E0B"],
};

const [activeProject, setActiveProject] = useState("marketing");
const userDefinedColors = projectPalettes[activeProject];
```

### User Preferences

```tsx
// Load user's saved colors from profile
const { user } = useAuth();
const [userDefinedColors, setUserDefinedColors] = useState(
  user.preferences.customColors || []
);
```

## Best Practices

1. **Provide Defaults**: Start with a set of useful default colors
2. **Limit Count**: Cap custom colors at 10-20 to avoid clutter
3. **Validate Input**: Ensure colors are valid before adding
4. **Persist Data**: Save colors to localStorage or backend
5. **Avoid Duplicates**: Check for duplicate colors before adding
6. **Visual Feedback**: Show color swatches clearly in the UI
7. **Accessibility**: Ensure sufficient contrast for readability

## Troubleshooting

### Colors Not Appearing

* Verify the `userDefinedColors` array is properly formatted
* Check that color values are valid CSS colors
* Ensure the array is passed to all color selector components

### Colors Not Persisting

* Confirm localStorage save logic is working
* Check for browser storage limits
* Verify localStorage keys are consistent

### Duplicate Colors

* Implement duplicate checking before adding colors
* Use `Set` to ensure uniqueness if needed

```tsx
const addColor = (color: string) => {
  setUserDefinedColors((prev) => {
    if (prev.includes(color)) return prev;
    return [...prev, color];
  });
};
```


# Custom Color Picker

Replace the built-in color picker with your own component

Every surface that renders a color picker accepts an optional `ColorSelector` prop, letting you replace the built-in picker with your own component. Use this to match your design system, integrate a third-party picker library, or restrict the palette users can choose from.

## Overview

The built-in `ColorSelector` (theme colors, standard colors, user-defined colors, hex input and eye dropper) is the default everywhere. Passing your own component to the `ColorSelector` prop swaps it at that surface:

| Component                 | Where the picker appears               |
| ------------------------- | -------------------------------------- |
| `TextColorSelector`       | Toolbar text color dropdown            |
| `BackgroundColorSelector` | Toolbar fill color dropdown            |
| `BorderSelector`          | Border color inside the border menu    |
| `BorderColorSelector`     | Standalone border color dropdown       |
| `SheetTabs`               | "Change color" in the tab context menu |
| `FloatingCellEditor`      | Text and fill color pickers            |

Because it is a plain prop, you can swap the picker on one surface and keep the default on the others.

## Writing a custom picker

Your component receives `ColorSelectorProps`:

```tsx
import type { ColorSelectorProps } from "@rowsncolumns/spreadsheet";

const BrandColorSelector = ({
  color,
  onChange,
  theme,
  resetButtonTitle = "Automatic",
  userDefinedColors,
  onAddUserDefinedColor,
}: ColorSelectorProps) => {
  const brandColors = ["#1E40AF", "#F59E0B", "#10B981", "#EF4444"];
  return (
    <div className="flex flex-col gap-1 p-2">
      <button onClick={() => onChange?.(undefined)}>{resetButtonTitle}</button>
      <div className="flex gap-1">
        {brandColors.map((value) => (
          <button
            key={value}
            aria-label={`Select ${value}`}
            style={{ backgroundColor: value, width: 24, height: 24 }}
            onClick={() => onChange?.(value)}
          />
        ))}
      </div>
    </div>
  );
};
```

`ColorSelectorProps`:

| Prop                    | Type                                  | Description                                                                                    |
| ----------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `color`                 | `Color \| null`                       | Currently selected color                                                                       |
| `onChange`              | `(color: Color \| undefined) => void` | Call with the new color; `undefined` resets to automatic                                       |
| `theme`                 | `SpreadsheetTheme`                    | Active theme, for rendering theme color swatches                                               |
| `resetButtonTitle`      | `string`                              | Label for the reset action ("Automatic", "No fill", "Reset")                                   |
| `userDefinedColors`     | `string[]`                            | Custom palette colors (see [User-Defined Colors](/configuration/features/user-defined-colors)) |
| `onAddUserDefinedColor` | `(color: string) => void`             | Add a color to the custom palette                                                              |

## Usage in the toolbar

```tsx
import {
  Toolbar,
  TextColorSelector,
  BackgroundColorSelector,
  BorderSelector,
} from "@rowsncolumns/spreadsheet";

<Toolbar>
  <TextColorSelector
    color={currentCellFormat?.textFormat?.color}
    theme={theme}
    onChange={handleTextColorChange}
    ColorSelector={BrandColorSelector}
  />
  <BackgroundColorSelector
    color={currentCellFormat?.backgroundColor}
    theme={theme}
    onChange={handleBackgroundColorChange}
    ColorSelector={BrandColorSelector}
  />
  <BorderSelector
    borders={currentCellFormat?.borders}
    theme={theme}
    onChange={onChangeBorder}
    ColorSelector={BrandColorSelector}
  />
</Toolbar>
```

`BorderSelector` forwards the component to the border color picker inside the borders menu.

## Sheet tab color picker

`SheetTabs` forwards the component to the "Change color" submenu of the tab context menu:

```tsx
import { SheetTabs } from "@rowsncolumns/spreadsheet";

<SheetTabs
  sheets={sheets}
  activeSheetId={activeSheetId}
  theme={theme}
  onChangeSheetTabColor={onChangeSheetTabColor}
  ColorSelector={BrandColorSelector}
/>
```

If you already supply a custom `ContextMenu` to `SheetTabs`, render your own picker inside it directly — the `ColorSelector` prop is forwarded to your context menu via `SheetTabContextMenuProps`.

## Floating cell editor

```tsx
import { FloatingCellEditor } from "@rowsncolumns/spreadsheet";

<FloatingCellEditor
  sheetId={activeSheetId}
  activeCell={activeCell}
  onChange={onChange}
  onChangeFormatting={onChangeFormatting}
  ColorSelector={BrandColorSelector}
/>
```

## Best practices

1. **Call `onChange` with `undefined`** for your reset action so "Automatic"/"No fill" behaves like the built-in picker
2. **Respect `resetButtonTitle`** — each surface passes the right label for its context
3. **Reuse one component** across surfaces for a consistent experience, or override only the surfaces you need
4. **Keep `userDefinedColors` support** if your app also uses the custom palette, so both features compose


# Version comparison

The version comparison feature allows users to compare two versions of a spreadsheet and visualize differences inline. Changes are highlighted with distinct colors for added, deleted, and modified cells.

<figure><img src="/files/MBrZ5cLx9fU83VPsQBjs" alt="Spreadsheet version comparison UI"><figcaption><p>Spreadsheet version comparison UI</p></figcaption></figure>

## Overview

Version comparison is useful for:

* Tracking changes between document revisions
* Reviewing edits before accepting them
* Understanding what changed between snapshots
* Collaboration workflows where multiple users edit the same document

## Visual Highlighting

| Change Type  | Background              | Text Color                 | Description                                                             |
| ------------ | ----------------------- | -------------------------- | ----------------------------------------------------------------------- |
| **Added**    | Light green (`#d0fae1`) | Dark green (`#046e38`)     | Cell exists in current version but not in previous                      |
| **Deleted**  | Light red (`#ffdbdb`)   | Dark red (`#b21313`)       | Cell exists in previous version but not in current (with strikethrough) |
| **Modified** | Both colors             | Red for old, green for new | Cell value or format changed                                            |

### Modified Cells

When a cell is modified, both the old and new values are displayed side by side within the cell:

* **Old value**: Red background with the previous formatting applied
* **New value**: Green background with the current formatting applied

This applies to both value changes and format-only changes (e.g., text becoming bold).

## Installation

```bash
npm install @rowsncolumns/version-comparison
```

## Basic Usage

```tsx
import { useState, useCallback, useMemo } from "react";
import { useVersionComparison } from "@rowsncolumns/version-comparison";
import { CanvasGrid, useSpreadsheetState } from "@rowsncolumns/spreadsheet";

const VersionComparisonDemo = () => {
  // Manage comparison mode yourself
  const [isComparing, setIsComparing] = useState(false);

  // Your current spreadsheet data
  const [sheetData, setSheetData] = useState(currentData);

  // Previous version snapshot
  const previousSheetData = useMemo(() => snapshotData, []);

  const { getCellData } = useSpreadsheetState({
    sheetData,
    // ... other props
  });

  // Function to get previous cell data
  const getPreviousCellData = useCallback(
    (sheetId: number, rowIndex: number, columnIndex: number) => {
      return previousSheetData[sheetId]?.[rowIndex]?.values?.[columnIndex] ?? null;
    },
    [previousSheetData]
  );

  // Version comparison hook - pass null when not comparing
  const { getCellDiff } = useVersionComparison({
    getCellData,
    getPreviousCellData: isComparing ? getPreviousCellData : null,
  });

  return (
    <div>
      {/* Comparison controls */}
      <div>
        {!isComparing ? (
          <button onClick={() => setIsComparing(true)}>Compare Versions</button>
        ) : (
          <button onClick={() => setIsComparing(false)}>Exit Comparison</button>
        )}
      </div>

      {/* Grid with comparison highlighting */}
      <CanvasGrid
        // ... standard props
        isComparing={isComparing}
        getCellDiff={getCellDiff}
      />
    </div>
  );
};
```

## API Reference

### useVersionComparison Hook

```typescript
const { getCellDiff } = useVersionComparison(options);
```

#### Options

```typescript
type UseVersionComparisonOptions<T extends CellData = CellData> = {
  /** Function to get current cell data */
  getCellData: (sheetId: number, rowIndex: number, columnIndex: number) => T | null | undefined;
  /** Function to get previous cell data (null = not comparing) */
  getPreviousCellData: ((sheetId: number, rowIndex: number, columnIndex: number) => T | null | undefined) | null;
  /**
   * Resolved effective `CellFormat` for the current version. The caller owns
   * all resolution — inline `ef` / `uf`, sid → cellXfs lookup, cellStyleStore
   * overlays, conditional formatting, etc. If omitted, format changes are
   * not detected (only value changes contribute to the diff).
   */
  getEffectiveFormat?: (sheetId: number, rowIndex: number, columnIndex: number) => CellFormat | null | undefined;
  /**
   * Resolved effective `CellFormat` for the previous version. Defaults to
   * `getEffectiveFormat` if omitted (useful when both sides share a resolver).
   */
  getPreviousEffectiveFormat?: (sheetId: number, rowIndex: number, columnIndex: number) => CellFormat | null | undefined;
  /** Shared strings map for current version */
  sharedStrings?: Map<string, string> | null;
  /** Shared strings map for previous version (defaults to sharedStrings) */
  previousSharedStrings?: Map<string, string> | null;
};
```

#### Return Values

| Property      | Type                                                   | Description                  |
| ------------- | ------------------------------------------------------ | ---------------------------- |
| `getCellDiff` | `(sheetId, rowIndex, columnIndex) => CellDiff \| null` | Get diff for a specific cell |

### CellDiff Type

```typescript
type CellDiff = {
  sheetId: number;
  rowIndex: number;
  columnIndex: number;
  state: "added" | "deleted" | "modified";
  detail?: CellDiffDetail;
};

type CellDiffDetail = {
  valueChanged: boolean;
  formatChanged: boolean;
  oldValue?: ExtendedValue;
  newValue?: ExtendedValue;
  oldFormattedValue?: string;
  newFormattedValue?: string;
  oldFormat?: CellFormat;
  newFormat?: CellFormat;
};
```

## Detecting Different Types of Changes

### Value Changes

```typescript
const diff = getCellDiff(sheetId, rowIndex, columnIndex);
if (diff?.detail?.valueChanged) {
  console.log("Old value:", diff.detail.oldFormattedValue);
  console.log("New value:", diff.detail.newFormattedValue);
}
```

### Format Changes

```typescript
const diff = getCellDiff(sheetId, rowIndex, columnIndex);
if (diff?.detail?.formatChanged) {
  console.log("Old format:", diff.detail.oldFormat);
  console.log("New format:", diff.detail.newFormat);
}
```

### Format-Only Changes

When a cell's value stays the same but formatting changes (e.g., text becomes bold):

```typescript
const diff = getCellDiff(sheetId, rowIndex, columnIndex);
const isFormatOnlyChange = diff?.detail?.formatChanged && !diff?.detail?.valueChanged;
if (isFormatOnlyChange) {
  // Show both old and new side by side to display formatting difference
}
```

## Working with Shared Strings

If your cell data uses shared strings (`ss` property pointing to a shared strings map), pass the shared strings:

```typescript
const { getCellDiff } = useVersionComparison({
  getCellData,
  getPreviousCellData,
  sharedStrings, // Map<string, string>
});
```

Cell data with shared string:

```typescript
// Cell uses ss to reference a shared string
const cellData = {
  ss: "0",  // References sharedStrings.get("0")
  fv: "Hello",
};

const sharedStrings = new Map([["0", "Hello"], ["1", "World"]]);
```

If the previous version has a different shared strings map (e.g., from a snapshot):

```typescript
const { getCellDiff } = useVersionComparison({
  getCellData,
  getPreviousCellData,
  sharedStrings: currentSharedStrings,
  previousSharedStrings: snapshotSharedStrings,
});
```

## Resolving Effective Formats

Format resolution is **caller-driven**. The hook never inspects `ef.sid` / `uf.sid` style references and never touches a cellXfs registry — it just compares the `CellFormat` objects you hand back. That keeps the hook agnostic to whatever resolution chain your app uses (cellXfs sid lookup, runtime overlays from a cell style store, conditional formatting, derived formats, …).

Pass `getEffectiveFormat` / `getPreviousEffectiveFormat` to opt in to format comparison:

```typescript
const { getCellDiff } = useVersionComparison({
  getCellData,
  getPreviousCellData,
  // Current side: useSpreadsheetState's getEffectiveFormat already merges
  // cellStyleStore + cellXfs sid + inline + derived format and is a
  // drop-in fit.
  getEffectiveFormat,
  // Previous side: walk the snapshot yourself when there's no
  // spreadsheet-state instance attached to it.
  getPreviousEffectiveFormat: (sheetId, row, col) => {
    const cell = previousSheetData[sheetId]?.[row]?.values?.[col];
    if (!cell) return null;
    const fmt = cell.ef ?? cell.uf;
    if (!fmt) return null;
    if ("sid" in fmt && typeof fmt.sid === "string") {
      return previousCellXfs.get(fmt.sid) ?? null;
    }
    return fmt;
  },
});
```

If both sides share a resolver, pass only `getEffectiveFormat` — `getPreviousEffectiveFormat` defaults to it. If you omit both, format changes simply aren't detected — only value changes will show up in the diff.

Cell data with a style reference looks like:

```typescript
// Cell uses ef.sid to reference an entry in the cellXfs map
const cellData = {
  ue: { sv: "Hello" },
  fv: "Hello",
  ef: { sid: "5" },  // Caller resolves: cellXfs.get("5")
};
```

## Comparison Summary

To display a summary of all changes:

```tsx
const DiffSummary = ({ isComparing, getCellDiff }) => {
  const summary = useMemo(() => {
    if (!isComparing) return { added: 0, modified: 0, deleted: 0 };

    let added = 0, modified = 0, deleted = 0;

    // Scan your data range
    for (let row = 0; row < rowCount; row++) {
      for (let col = 0; col < columnCount; col++) {
        const diff = getCellDiff(sheetId, row, col);
        if (diff?.state === "added") added++;
        else if (diff?.state === "modified") modified++;
        else if (diff?.state === "deleted") deleted++;
      }
    }

    return { added, modified, deleted };
  }, [isComparing, getCellDiff]);

  return (
    <div>
      <span>+{summary.added} added</span>
      <span>~{summary.modified} modified</span>
      <span>-{summary.deleted} deleted</span>
    </div>
  );
};
```

## CanvasGrid Props

| Prop          | Type                                      | Description                      |
| ------------- | ----------------------------------------- | -------------------------------- |
| `isComparing` | `boolean`                                 | Enable comparison mode rendering |
| `getCellDiff` | `(sheetId, row, col) => CellDiff \| null` | Function to get cell diff        |

## Diff Engine Functions

For advanced use cases, you can use the diff engine functions directly:

```typescript
import { areCellValuesEqual, areCellFormatsEqual } from "@rowsncolumns/version-comparison";

// Compare values
const valuesEqual = areCellValuesEqual(
  { nv: 100 },
  { nv: 100 }
); // true

// Compare formats
const formatsEqual = areCellFormatsEqual(
  { textFormat: { bold: true } },
  { textFormat: { bold: false } }
); // false
```

## Best Practices

1. **Snapshot Management**: Store snapshots efficiently - consider only storing changed cells rather than the entire sheet.
2. **Performance**: For large spreadsheets, consider lazy-loading diffs only for visible cells.
3. **User Experience**:
   * Show a clear indicator when comparison mode is active
   * Provide a summary of changes
   * Allow users to navigate between changes
4. **Format Comparison**: Remember that format changes count as modifications even if the value is unchanged.

## Example: Full Implementation

```tsx
import React, { useCallback, useMemo, useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  useSpreadsheetState,
} from "@rowsncolumns/spreadsheet";
import { useVersionComparison } from "@rowsncolumns/version-comparison";

const VersionComparisonApp = () => {
  // Manage comparison mode yourself
  const [isComparing, setIsComparing] = useState(false);

  const [sheetData, setSheetData] = useState(currentSheetData);
  const previousSheetData = useMemo(() => snapshotSheetData, []);

  const { getCellData, getEffectiveFormat, activeSheetId, ...spreadsheetProps } = useSpreadsheetState({
    sheetData,
    onChangeSheetData: setSheetData,
    // ... other props
  });

  const getPreviousCellData = useCallback(
    (sheetId, rowIndex, columnIndex) =>
      previousSheetData[sheetId]?.[rowIndex]?.values?.[columnIndex] ?? null,
    [previousSheetData]
  );

  // Resolve effective format for the snapshot (sid → previousCellXfs).
  // There's no useSpreadsheetState instance attached to the snapshot, so
  // walk it ourselves.
  const getPreviousEffectiveFormat = useCallback(
    (sheetId, rowIndex, columnIndex) => {
      const cell = getPreviousCellData(sheetId, rowIndex, columnIndex);
      if (!cell) return null;
      const fmt = cell.ef ?? cell.uf;
      if (!fmt) return null;
      if ("sid" in fmt && typeof fmt.sid === "string") {
        return previousCellXfs.get(fmt.sid) ?? null;
      }
      return fmt;
    },
    [getPreviousCellData, previousCellXfs]
  );

  // Pass null when not comparing to disable diffing
  const { getCellDiff } = useVersionComparison({
    getCellData,
    getPreviousCellData: isComparing ? getPreviousCellData : null,
    getEffectiveFormat,
    getPreviousEffectiveFormat,
  });

  return (
    <SpreadsheetProvider>
      <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
        {/* Comparison toolbar */}
        <div style={{ padding: 8, borderBottom: "1px solid #ddd" }}>
          {!isComparing ? (
            <button onClick={() => setIsComparing(true)}>Compare with Previous</button>
          ) : (
            <button onClick={() => setIsComparing(false)}>Exit Comparison</button>
          )}
        </div>

        {/* Spreadsheet grid */}
        <CanvasGrid
          sheetId={activeSheetId}
          {...spreadsheetProps}
          getCellData={getCellData}
          isComparing={isComparing}
          getCellDiff={getCellDiff}
        />
      </div>
    </SpreadsheetProvider>
  );
};
```

## Related Features

* [Undo/Redo](/configuration/features/undo-redo) - Track and revert changes
* [Real-time Data](/configuration/features/real-time-data) - Collaborative editing
* [Cell Renderer](/configuration/features/cell-renderer) - Custom cell rendering


# Navigate to Sheet Range

Programmatically navigate and scroll to specific cell ranges

The `useNavigateToSheetRange` hook provides a programmatic way to navigate to specific cell ranges, change sheets, and scroll to particular locations in your spreadsheet. This is useful for implementing search functionality, jumping to errors, following links, and creating guided tours.

## Overview

Navigate to Sheet Range enables:

* **Sheet switching**: Change the active sheet programmatically
* **Cell navigation**: Jump to specific cells or ranges
* **Auto-scrolling**: Automatically scroll cells into view
* **Selection updates**: Update selections when navigating
* **Cross-sheet navigation**: Navigate across different sheets

## Basic Usage

```tsx
import { SpreadsheetProvider } from "@rowsncolumns/spreadsheet";
import { useNavigateToSheetRange } from "@rowsncolumns/spreadsheet";

function MySpreadsheet() {
  const navigateToSheetRange = useNavigateToSheetRange();

  const handleNavigate = () => {
    navigateToSheetRange?.({
      sheetId: 2,
      startRowIndex: 10,
      startColumnIndex: 5,
      endRowIndex: 15,
      endColumnIndex: 10,
    });
  };

  return (
    <button onClick={handleNavigate}>
      Go to Sheet 2, Range F11:K16
    </button>
  );
}
```

## Hook Usage

The hook must be used within a `SpreadsheetProvider`:

```tsx
import { SpreadsheetProvider } from "@rowsncolumns/spreadsheet";
import { useNavigateToSheetRange } from "@rowsncolumns/spreadsheet";

function NavigationComponent() {
  const navigateToSheetRange = useNavigateToSheetRange();

  // navigateToSheetRange is available for use
  return <YourComponent navigate={navigateToSheetRange} />;
}

function App() {
  return (
    <SpreadsheetProvider>
      <NavigationComponent />
    </SpreadsheetProvider>
  );
}
```

## Function Signature

```typescript
type SheetRange = {
  sheetId: number;
  startRowIndex: number;
  startColumnIndex: number;
  endRowIndex: number;
  endColumnIndex: number;
};

type NavigateToSheetRange = (range: SheetRange) => void;
```

## Navigation Examples

### Navigate to Specific Cell

```tsx
const navigateToCell = (sheetId: number, rowIndex: number, columnIndex: number) => {
  navigateToSheetRange?.({
    sheetId,
    startRowIndex: rowIndex,
    startColumnIndex: columnIndex,
    endRowIndex: rowIndex,
    endColumnIndex: columnIndex,
  });
};

// Navigate to cell B5 on sheet 1
navigateToCell(1, 5, 2);
```

### Navigate to Range

```tsx
const navigateToRange = () => {
  // Navigate to range A1:E10 on sheet 3
  navigateToSheetRange?.({
    sheetId: 3,
    startRowIndex: 1,
    startColumnIndex: 1,
    endRowIndex: 10,
    endColumnIndex: 5,
  });
};
```

### Navigate to Named Range

```tsx
function navigateToNamedRange(namedRangeName: string, namedRanges: NamedRange[]) {
  const namedRange = namedRanges.find((nr) => nr.name === namedRangeName);

  if (!namedRange || !namedRange.range) {
    console.error(`Named range "${namedRangeName}" not found`);
    return;
  }

  navigateToSheetRange?.(namedRange.range);
}

// Navigate to named range "SalesData"
navigateToNamedRange("SalesData", namedRanges);
```

## Complete Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Sheet,
  useNavigateToSheetRange,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
} from "@rowsncolumns/spreadsheet-state";

function NavigationControls() {
  const navigateToSheetRange = useNavigateToSheetRange();

  const quickJumps = [
    { label: "Go to A1", sheetId: 1, row: 1, col: 1 },
    { label: "Go to Z100", sheetId: 1, row: 100, col: 26 },
    { label: "Sheet 2 - F11:K16", sheetId: 2, row: 11, col: 6, endRow: 16, endCol: 11 },
  ];

  const handleQuickJump = (jump: typeof quickJumps[0]) => {
    navigateToSheetRange?.({
      sheetId: jump.sheetId,
      startRowIndex: jump.row,
      startColumnIndex: jump.col,
      endRowIndex: jump.endRow || jump.row,
      endColumnIndex: jump.endCol || jump.col,
    });
  };

  return (
    <div className="flex gap-2 p-2">
      {quickJumps.map((jump, index) => (
        <button
          key={index}
          onClick={() => handleQuickJump(jump)}
          className="px-3 py-1 bg-blue-500 text-white rounded"
        >
          {jump.label}
        </button>
      ))}
    </div>
  );
}

function SpreadsheetWithNavigation() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 200, columnCount: 26, title: "Sheet 1" },
    { sheetId: 2, rowCount: 200, columnCount: 26, title: "Sheet 2" },
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
    onChangeActiveSheet,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  return (
    <SpreadsheetProvider>
      <div className="flex flex-col h-screen">
        <NavigationControls />

        <div className="flex-1">
          <CanvasGrid
            sheetId={activeSheetId}
            activeCell={activeCell}
            selections={selections}
            getCellData={getCellData}
            onChangeActiveCell={onChangeActiveCell}
            onChangeSelections={onChangeSelections}
            onChangeActiveSheet={onChangeActiveSheet}
          />
        </div>
      </div>
    </SpreadsheetProvider>
  );
}

export default SpreadsheetWithNavigation;
```

## Use Cases

### Search Results Navigation

Navigate to cells matching search criteria:

```tsx
function SearchAndNavigate() {
  const navigateToSheetRange = useNavigateToSheetRange();
  const [searchResults, setSearchResults] = useState<SheetRange[]>([]);
  const [currentIndex, setCurrentIndex] = useState(0);

  const handleNextResult = () => {
    if (searchResults.length === 0) return;

    const nextIndex = (currentIndex + 1) % searchResults.length;
    setCurrentIndex(nextIndex);
    navigateToSheetRange?.(searchResults[nextIndex]);
  };

  const handlePreviousResult = () => {
    if (searchResults.length === 0) return;

    const prevIndex = currentIndex === 0 ? searchResults.length - 1 : currentIndex - 1;
    setCurrentIndex(prevIndex);
    navigateToSheetRange?.(searchResults[prevIndex]);
  };

  return (
    <div>
      <button onClick={handlePreviousResult}>Previous</button>
      <span>{currentIndex + 1} / {searchResults.length}</span>
      <button onClick={handleNextResult}>Next</button>
    </div>
  );
}
```

### Error Navigation

Jump to cells with formula errors:

```tsx
function navigateToErrors(
  sheets: Sheet[],
  sheetData: SheetData<CellData>,
  navigateToSheetRange: NavigateToSheetRange
) {
  const errors: SheetRange[] = [];

  sheets.forEach((sheet) => {
    const data = sheetData[sheet.sheetId];
    if (!data) return;

    data.forEach((row, rowIndex) => {
      if (!row?.values) return;

      row.values.forEach((cell, colIndex) => {
        if (cell?.ev?.ev) {
          errors.push({
            sheetId: sheet.sheetId,
            startRowIndex: rowIndex,
            startColumnIndex: colIndex,
            endRowIndex: rowIndex,
            endColumnIndex: colIndex,
          });
        }
      });
    });
  });

  // Navigate to first error
  if (errors.length > 0) {
    navigateToSheetRange(errors[0]);
  }

  return errors;
}
```

### Table Navigation

Navigate to different sections of a table:

```tsx
function navigateToTable(table: TableView, section: "header" | "data" | "total") {
  const range = { ...table.range };

  switch (section) {
    case "header":
      range.endRowIndex = range.startRowIndex;
      break;
    case "data":
      range.startRowIndex = range.startRowIndex + 1;
      if (table.totalsRow) {
        range.endRowIndex = range.endRowIndex - 1;
      }
      break;
    case "total":
      if (table.totalsRow) {
        range.startRowIndex = range.endRowIndex;
      }
      break;
  }

  navigateToSheetRange?.(range);
}
```

### Hyperlink Navigation

Navigate when clicking on cell references:

```tsx
function handleCellReferenceClick(cellReference: string) {
  // Parse cell reference like "Sheet2!A1" or "B5"
  const match = cellReference.match(/^(?:(.+)!)?([A-Z]+)(\d+)$/);

  if (!match) return;

  const [, sheetName, column, row] = match;
  const sheetId = sheetName ? getSheetIdByName(sheetName) : activeSheetId;
  const columnIndex = columnToIndex(column);
  const rowIndex = parseInt(row, 10);

  navigateToSheetRange?.({
    sheetId,
    startRowIndex: rowIndex,
    startColumnIndex: columnIndex,
    endRowIndex: rowIndex,
    endColumnIndex: columnIndex,
  });
}
```

### Guided Tour

Create a step-by-step tour of your spreadsheet:

```tsx
function SpreadsheetTour() {
  const navigateToSheetRange = useNavigateToSheetRange();

  const tourSteps = [
    {
      title: "Welcome",
      range: { sheetId: 1, startRowIndex: 1, startColumnIndex: 1, endRowIndex: 1, endColumnIndex: 1 },
      description: "This is cell A1, the starting point",
    },
    {
      title: "Sales Data",
      range: { sheetId: 1, startRowIndex: 5, startColumnIndex: 2, endRowIndex: 20, endColumnIndex: 5 },
      description: "Here is our sales data table",
    },
    {
      title: "Summary",
      range: { sheetId: 2, startRowIndex: 1, startColumnIndex: 1, endRowIndex: 10, endColumnIndex: 3 },
      description: "The summary is on Sheet 2",
    },
  ];

  const [currentStep, setCurrentStep] = useState(0);

  const goToStep = (stepIndex: number) => {
    if (stepIndex < 0 || stepIndex >= tourSteps.length) return;

    setCurrentStep(stepIndex);
    navigateToSheetRange?.(tourSteps[stepIndex].range);
  };

  return (
    <div className="p-4 border rounded">
      <h3 className="font-bold mb-2">{tourSteps[currentStep].title}</h3>
      <p className="text-sm mb-4">{tourSteps[currentStep].description}</p>

      <div className="flex gap-2">
        <button
          onClick={() => goToStep(currentStep - 1)}
          disabled={currentStep === 0}
          className="px-3 py-1 bg-gray-500 text-white rounded disabled:opacity-50"
        >
          Previous
        </button>
        <button
          onClick={() => goToStep(currentStep + 1)}
          disabled={currentStep === tourSteps.length - 1}
          className="px-3 py-1 bg-blue-500 text-white rounded disabled:opacity-50"
        >
          Next
        </button>
      </div>

      <div className="text-sm text-gray-500 mt-2">
        Step {currentStep + 1} of {tourSteps.length}
      </div>
    </div>
  );
}
```

## Advanced Features

### Smooth Scrolling

Implement smooth scrolling with animation:

```tsx
function smoothNavigate(range: SheetRange, duration: number = 300) {
  // Implementation would involve animating the scroll position
  // This is handled internally by the spreadsheet component
  navigateToSheetRange?.(range);
}
```

### Navigation History

Track navigation history for back/forward buttons:

```tsx
function useNavigationHistory() {
  const [history, setHistory] = useState<SheetRange[]>([]);
  const [currentIndex, setCurrentIndex] = useState(-1);
  const navigateToSheetRange = useNavigateToSheetRange();

  const navigate = (range: SheetRange) => {
    const newHistory = history.slice(0, currentIndex + 1);
    newHistory.push(range);
    setHistory(newHistory);
    setCurrentIndex(newHistory.length - 1);
    navigateToSheetRange?.(range);
  };

  const goBack = () => {
    if (currentIndex > 0) {
      const newIndex = currentIndex - 1;
      setCurrentIndex(newIndex);
      navigateToSheetRange?.(history[newIndex]);
    }
  };

  const goForward = () => {
    if (currentIndex < history.length - 1) {
      const newIndex = currentIndex + 1;
      setCurrentIndex(newIndex);
      navigateToSheetRange?.(history[newIndex]);
    }
  };

  return {
    navigate,
    goBack,
    goForward,
    canGoBack: currentIndex > 0,
    canGoForward: currentIndex < history.length - 1,
  };
}
```

## Helper Functions

### Cell Address to Range

Convert cell address notation to range:

```tsx
import { cellToAddress } from "@rowsncolumns/utils";

function addressToRange(address: string, sheetId: number): SheetRange | null {
  // Parse address like "A1" or "Sheet2!B5:C10"
  const match = address.match(/^(?:(.+)!)?([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/);

  if (!match) return null;

  const [, sheetName, startCol, startRow, endCol, endRow] = match;
  const actualSheetId = sheetName ? getSheetIdByName(sheetName) : sheetId;

  return {
    sheetId: actualSheetId,
    startRowIndex: parseInt(startRow, 10),
    startColumnIndex: columnToIndex(startCol),
    endRowIndex: parseInt(endRow || startRow, 10),
    endColumnIndex: columnToIndex(endCol || startCol),
  };
}
```

## Best Practices

1. **Validate Ranges**: Ensure row and column indices are within bounds
2. **Handle Edge Cases**: Check for undefined or null navigateToSheetRange
3. **User Feedback**: Provide visual indicators during navigation
4. **Accessibility**: Support keyboard shortcuts for navigation
5. **Performance**: Avoid excessive navigation calls in loops

## Troubleshooting

### Navigation Not Working

* Verify you're calling the hook within `SpreadsheetProvider`
* Check that sheet IDs and indices are valid
* Ensure the component is properly mounted

### Sheet Not Switching

* Confirm the target sheet exists in the sheets array
* Verify `onChangeActiveSheet` is properly connected
* Check that sheet IDs match

### Scroll Position Not Updating

* Ensure row and column indices are within the sheet bounds
* Verify the grid is properly rendered
* Check for conflicting scroll handlers


# Calculated columns

Add columns with formulas, join multiple tables

Add any formula on the far end of the table, to create a calculated column. Calculated columns are indicated with a `bolt` icon.

<figure><img src="/files/VOZor8QvhKDbbXb4kvg4" alt=""><figcaption><p>Calculated column</p></figcaption></figure>

When table expands, while user adds new row, calculated columns are automatically added.


# Structured references

Add structured data to Spreadsheet and reference them using semantic formulas

Spreadsheet allows you to convert any data in the sheet to an Excel Table. Select a dataset, or add a dataset using an Array formula such as `=IMPORTDATA`

<figure><img src="/files/TqD1AtA6WleLOcFLiT2f" alt=""><figcaption><p>Convert dataset to Structured table</p></figcaption></figure>

You can now add formulas in your table referencing to the column names

<figure><img src="/files/6RNh89TgUKYydO4L3ufN" alt=""><figcaption><p>Formulas referencing column names</p></figcaption></figure>

## Calculated Columns

Dynamic columns can be added just like Excel. Users can edit the table, add multiple columns and extend the table range to give the column additional features such as filtering, sorting etc

<figure><img src="/files/2ZIV3rvN8LJTCHXLxGxS" alt=""><figcaption></figcaption></figure>

## Joining tables

You can join tables and create dynamic columns from multiple datasources. Structured formulas refer tables using `Table name` . For example, in the above table, you can add formulas like `=Table 2[@Region]`


# Schema based tables and columns

Convert any dataset to a schema driven table

Tables in Spreadsheet 2 gives you the same amount of control that you get in Excel/Google sheets. Users can add dynamic columns, calculated columns with a formula, create columns from another table etc.

## Column order

Column orders can be easily changed by selecting a column from the column header and moving them left or right.

All formulas and references are automatically updated.

<figure><img src="/files/aCU7nJr8NzdCBHM36QwY" alt=""><figcaption><p>Re-arranging columns</p></figcaption></figure>


# Drag and Drop

Drag and drop images and CSV/Excel files from your desktop

With drag and drop enabled, users can move files from their desktop directly to the sheets.

{% hint style="info" %}
Images are stored as base64 string while CSV files are converted to sheetData
{% endhint %}

<figure><img src="/files/mztBPg8T2uqddczJ4izg" alt=""><figcaption><p>Drag and drop</p></figcaption></figure>

{% hint style="warning" %}
Dropping an Excel file will replace all sheets, sheet data, tables, embeds, charts etc which is not undoable
{% endhint %}


# Text to Columns

Split text in cells into multiple columns

The Text to Columns feature allows users to split text content from cells into multiple columns based on delimiters or fixed widths. This is useful for importing data, cleaning up datasets, and reformatting text.

## Overview

Text to Columns functionality enables users to:

* Split comma-separated values (CSV) into columns
* Parse tab-delimited or space-delimited text
* Split by custom delimiters
* Break up formatted text (e.g., "First Last" � "First" | "Last")
* Clean and restructure imported data

## Basic Usage

The `onSplitTextToColumns` callback is provided by `useSpreadsheetState` and is triggered when users request to split text in cells.

```tsx
import {
  SpreadsheetProvider,
  CanvasGrid,
} from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function SpreadsheetWithTextSplit() {
  const {
    activeCell,
    activeSheetId,
    selections,
    onSplitTextToColumns,
    // ... other state
  } = useSpreadsheetState({
    // configuration
  });

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        onSplitTextToColumns={onSplitTextToColumns}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

## Function Signature

```typescript
type OnSplitTextToColumns = (
  sheetId: number,
  activeCell: CellInterface,
  selections: SelectionArea<SelectionAttributes>[]
) => void;
```

## How It Works

When `onSplitTextToColumns` is called:

1. The function analyzes the selected cells
2. Detects or prompts for a delimiter (comma, semicolon, tab, space, etc.)
3. Splits the text content based on the delimiter
4. Distributes the split values across adjacent columns
5. Preserves the original data structure

## Example Usage

### Manual Trigger

You can manually trigger text splitting from a button or menu:

```tsx
function SpreadsheetToolbar() {
  const {
    activeCell,
    activeSheetId,
    selections,
    onSplitTextToColumns,
  } = useSpreadsheetState({
    // configuration
  });

  const handleSplitText = () => {
    onSplitTextToColumns?.(activeSheetId, activeCell, selections);
  };

  return (
    <button
      onClick={handleSplitText}
      className="px-3 py-1 bg-blue-500 text-white rounded"
    >
      Split Text to Columns
    </button>
  );
}
```

### Context Menu Integration

Add Text to Columns to the context menu:

```tsx
import { DropdownMenu, DropdownMenuItem } from "@rowsncolumns/ui";

function CustomContextMenu({
  activeCell,
  activeSheetId,
  selections,
  onSplitTextToColumns,
}) {
  return (
    <DropdownMenu>
      <DropdownMenuItem
        onClick={() => onSplitTextToColumns?.(activeSheetId, activeCell, selections)}
      >
        Split Text to Columns
      </DropdownMenuItem>
      {/* Other menu items */}
    </DropdownMenu>
  );
}
```

## Common Use Cases

### Splitting Names

Split "First Last" into separate columns:

```tsx
// Before:
// | John Doe | Jane Smith |

// After splitting by space:
// | John | Doe | Jane | Smith |
```

### Parsing CSV Data

Split comma-separated values:

```tsx
// Before:
// | apple,banana,cherry |

// After splitting by comma:
// | apple | banana | cherry |
```

### Processing Addresses

Split full addresses into components:

```tsx
// Before:
// | 123 Main St, New York, NY 10001 |

// After splitting by comma:
// | 123 Main St | New York | NY 10001 |
```

## Complete Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Toolbar,
  ToolbarIconButton,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
} from "@rowsncolumns/spreadsheet-state";

function SpreadsheetWithSplitText() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Data" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
    onChange,
    onSplitTextToColumns,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  const handleSplitText = () => {
    if (!onSplitTextToColumns) {
      console.warn("Split text to columns is not available");
      return;
    }

    onSplitTextToColumns(activeSheetId, activeCell, selections);
  };

  return (
    <SpreadsheetProvider>
      <div className="flex flex-col h-screen">
        <Toolbar>
          <ToolbarIconButton
            onClick={handleSplitText}
            title="Split Text to Columns"
          >
            <SplitIcon />
          </ToolbarIconButton>
        </Toolbar>

        <div className="flex-1">
          <CanvasGrid
            sheetId={activeSheetId}
            activeCell={activeCell}
            selections={selections}
            getCellData={getCellData}
            onChangeActiveCell={onChangeActiveCell}
            onChangeSelections={onChangeSelections}
            onChange={onChange}
            onSplitTextToColumns={onSplitTextToColumns}
          />
        </div>
      </div>
    </SpreadsheetProvider>
  );
}

export default SpreadsheetWithSplitText;
```

## Custom Implementation

If you need custom text splitting logic, you can implement your own:

```tsx
import { CellInterface, SelectionArea } from "@rowsncolumns/spreadsheet";

function customSplitTextToColumns(
  sheetId: number,
  activeCell: CellInterface,
  selections: SelectionArea<SelectionAttributes>[],
  delimiter: string = ","
) {
  // Get the selection range
  const selection = selections[0] || {
    range: {
      startRowIndex: activeCell.rowIndex,
      endRowIndex: activeCell.rowIndex,
      startColumnIndex: activeCell.columnIndex,
      endColumnIndex: activeCell.columnIndex,
    },
  };

  // Process each row in the selection
  for (
    let rowIndex = selection.range.startRowIndex;
    rowIndex <= selection.range.endRowIndex;
    rowIndex++
  ) {
    // Get the cell value
    const cellData = getCellData(
      sheetId,
      rowIndex,
      selection.range.startColumnIndex
    );

    if (!cellData?.formattedValue) continue;

    // Split the text
    const parts = cellData.formattedValue.split(delimiter);

    // Write parts to adjacent columns
    parts.forEach((part, index) => {
      const columnIndex = selection.range.startColumnIndex + index;

      // Update cell with split value
      onChange?.(
        sheetId,
        { rowIndex, columnIndex },
        part.trim(),
        undefined,
        false
      );
    });
  }
}
```

## Delimiter Options

Common delimiters for text splitting:

```typescript
const delimiters = {
  comma: ",",
  semicolon: ";",
  tab: "\t",
  space: " ",
  pipe: "|",
  colon: ":",
  custom: "...", // User-defined
};
```

## Advanced Features

### Multiple Delimiters

Split by multiple delimiters at once:

```tsx
function splitByMultipleDelimiters(text: string, delimiters: string[]) {
  let result = [text];

  delimiters.forEach((delimiter) => {
    result = result.flatMap((part) => part.split(delimiter));
  });

  return result.map((part) => part.trim()).filter(Boolean);
}

// Example: Split by comma OR semicolon
const parts = splitByMultipleDelimiters("apple,banana;cherry", [",", ";"]);
// Result: ["apple", "banana", "cherry"]
```

### Fixed Width Splitting

Split text at fixed character positions:

```tsx
function splitFixedWidth(text: string, widths: number[]) {
  const parts: string[] = [];
  let position = 0;

  widths.forEach((width) => {
    parts.push(text.slice(position, position + width).trim());
    position += width;
  });

  // Add remaining text
  if (position < text.length) {
    parts.push(text.slice(position).trim());
  }

  return parts;
}

// Example: Split "JohnDoe  25Engineer"
const parts = splitFixedWidth("JohnDoe  25Engineer", [8, 4]);
// Result: ["JohnDoe", "25", "Engineer"]
```

### Preserve Quoted Text

Don't split text within quotes:

```tsx
function splitWithQuotes(text: string, delimiter: string) {
  const regex = new RegExp(
    `${delimiter}(?=(?:[^"]*"[^"]*")*[^"]*$)`,
    "g"
  );
  return text.split(regex).map((part) => part.replace(/^"|"$/g, "").trim());
}

// Example: Split 'apple,"banana,cherry",grape'
const parts = splitWithQuotes('apple,"banana,cherry",grape', ",");
// Result: ["apple", "banana,cherry", "grape"]
```

## Keyboard Shortcuts

Add a keyboard shortcut for text splitting:

```tsx
function SpreadsheetWithShortcuts() {
  const { onSplitTextToColumns } = useSpreadsheetState({
    // configuration
  });

  const handleKeyDown = (e: React.KeyboardEvent) => {
    // Ctrl/Cmd + Shift + T
    if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "T") {
      e.preventDefault();
      onSplitTextToColumns?.(activeSheetId, activeCell, selections);
    }
  };

  return (
    <CanvasGrid
      onKeyDown={handleKeyDown}
      onSplitTextToColumns={onSplitTextToColumns}
      // ... other props
    />
  );
}
```

## Best Practices

1. **Backup Data**: Always preserve original data before splitting
2. **Check Column Space**: Ensure enough empty columns for split data
3. **Handle Edge Cases**: Account for empty cells and malformed data
4. **Trim Whitespace**: Remove leading/trailing spaces from split values
5. **Validate Output**: Verify the split operation produced expected results
6. **Undo Support**: Ensure split operations are undoable

## Error Handling

```tsx
function safeSplitTextToColumns(
  sheetId: number,
  activeCell: CellInterface,
  selections: SelectionArea<SelectionAttributes>[]
) {
  try {
    // Check if there's a selection
    if (!selections.length) {
      console.warn("No cells selected for splitting");
      return;
    }

    // Check if there's enough space for split data
    const estimatedColumns = 5; // Estimate or calculate
    const maxColumn = getSheetColumnCount(sheetId);
    const startColumn = activeCell.columnIndex;

    if (startColumn + estimatedColumns > maxColumn) {
      console.error("Not enough columns for split operation");
      return;
    }

    // Perform split
    onSplitTextToColumns?.(sheetId, activeCell, selections);
  } catch (error) {
    console.error("Error splitting text to columns:", error);
  }
}
```

## Limitations

* The split operation requires adjacent empty columns
* Very long text may exceed column limits
* Complex delimiters may require custom parsing
* Large selections may impact performance

## Use Cases

### Data Import

Clean up imported CSV or TSV data:

```tsx
// Import raw data as single column, then split
```

### Name Parsing

Split full names into first and last names:

```tsx
// "John Doe" � "John" | "Doe"
```

### Email Processing

Extract username and domain from emails:

```tsx
// "user@example.com" � "user" | "example.com"
```

### Tag Parsing

Split tags or categories:

```tsx
// "tag1,tag2,tag3" � "tag1" | "tag2" | "tag3"
```

## Troubleshooting

### Split Not Working

* Verify `onSplitTextToColumns` is passed to CanvasGrid
* Check that cells contain text data
* Ensure there are empty columns for split data

### Unexpected Results

* Check delimiter selection
* Verify text format (quotes, escapes, etc.)
* Test with sample data first

### Performance Issues

* Limit selection size for large datasets
* Consider batch processing for many cells
* Use web workers for heavy split operations


# Conditional formatting

Change text or background colour of cells based on the value

CanvasGrid accepts array of `conditionalFormats` prop which can be used to inject formatting rules based on the value of a cell. Formula values are supported.

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";

const MySpreadsheet = () => {
  return (
    <CanvasGrid
      conditionalFormats={[
        {
          ranges: [
            {
              startRowIndex: 1,
              endRowIndex: 1000,
              startColumnIndex: 2,
              endColumnIndex: 2,
            },
          ],
          booleanRule: {
            condition: {
              type: "NUMBER_LESS",
              values: [
                {
                  userEnteredValue: "0",
                },
              ],
            },
            format: {
              backgroundColor: "red",
            },
          },
        },
      ]}
    />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);

```

### Gradient scale or heatmaps

```typescript
conditionalFormats: [
  {
    ranges: [
      {
        startRowIndex: 11,
        endRowIndex: 26,
        startColumnIndex: 7,
        endColumnIndex: 8,
      },
    ],
    gradientRule: {
      minpoint: {
        color: "#FFCF54",
        type: "MIN",
        // value: 10 Automatically calculated from range
      },
      midpoint: {
        color: "#FFE8A3",
        type: "MIN",
        // value: 12, Automatically calculated from range
      },
      maxpoint: {
        color: "#FFFFFF",
        type: "MAX",
        // value: 21, Automatically calculated from range
      },
    },
  },
],
```

### Data bars

Paint a horizontal bar inside each cell, scaled by the cell's value relative to the range min/max. Round-trips XLSX + ODS.

```typescript
conditionalFormats: [
  {
    ranges: [
      {
        startRowIndex: 1,
        endRowIndex: 20,
        startColumnIndex: 3,
        endColumnIndex: 3,
      },
    ],
    dataBarRule: {
      color: "#638EC6",
      minpoint: { type: "MIN" },
      maxpoint: { type: "MAX" },
      // Optional: separate color for negative values
      negativeColor: "#DC2626",
      // Optional: render direction ("leftToRight" / "rightToLeft")
      direction: "leftToRight",
      // Optional: hide the cell value, show only the bar
      showValue: true,
    },
  },
],
```

### Icon sets

Stamp an icon to the left of each cell based on which bucket the value falls in. Many built-in icon families ship: `3TrafficLights1`, `3TrafficLights2`, `3Arrows`, `3Symbols`, `4Rating`, `5Rating`, `5Arrows`, etc.

```typescript
conditionalFormats: [
  {
    ranges: [
      {
        startRowIndex: 1,
        endRowIndex: 20,
        startColumnIndex: 4,
        endColumnIndex: 4,
      },
    ],
    iconSetRule: {
      iconSet: "3TrafficLights1",
      // Thresholds split the value range into N buckets — for a 3-icon
      // set we need 2 thresholds (low/mid/high).
      thresholds: [
        { type: "PERCENT", value: 33 },
        { type: "PERCENT", value: 66 },
      ],
      // Optional: reverse the icon order (so red lights up on high
      // values instead of low).
      reverse: false,
      // Optional: show only the icon, hide the cell value
      showValue: true,
    },
  },
],
```

## Supported conditions

```typescript
// Supported condition types
export const CONDITION_TYPES = [
  "NUMBER_GREATER",
  "NUMBER_GREATER_THAN_EQ",
  "NUMBER_LESS",
  "NUMBER_LESS_THAN_EQ",
  "NUMBER_EQ",
  "NUMBER_NOT_EQ",
  "NUMBER_BETWEEN",
  "NUMBER_NOT_BETWEEN",
  "TEXT_CONTAINS",
  "TEXT_NOT_CONTAINS",
  "TEXT_STARTS_WITH",
  "TEXT_ENDS_WITH",
  "TEXT_EQ",
  "TEXT_IS_EMAIL",
  "TEXT_IS_URL",
  "DATE_EQ",
  "DATE_BEFORE",
  "DATE_AFTER",
  "DATE_ON_OR_BEFORE",
  "DATE_ON_OR_AFTER",
  "DATE_BETWEEN",
  "DATE_NOT_BETWEEN",
  "DATE_IS_VALID",
  "ONE_OF_RANGE",
  "ONE_OF_LIST",
  "BLANK",
  "NOT_BLANK",
  "CUSTOM_FORMULA",
  "BOOLEAN",
  "TEXT_NOT_EQ",
  "DATE_NOT_EQ",
] as const;
```

## Adding conditional format editor UI

Spreadsheet comes with a default conditional format editor.

Right click on any cell and select `Conditional format editor` . Or you can invoke the function `onRequestConditionalFormat` from useSpreadsheetState hook

<figure><img src="/files/1dASGN09BWrNJTQPAxMe" alt=""><figcaption><p>Conditional format editor</p></figcaption></figure>

```tsx
import { SpreadsheetProvider, CanvasGrid, defaultSpreadsheetTheme } from "@rowsncolumns/spreadsheet";
import { ConditionalFormatDialog, ConditionalFormatEditor } from "@rowsncolumns/spreadsheet-state"

const MySpreadsheet = () => {
  const [conditionalFormats, onChangeConditionalFormats] = useState<
      ConditionalFormatRule[]
    >([]);
  const [theme, onChangeTheme] = useState<SpreadsheetTheme>(
    defaultSpreadsheetTheme
  );
  const {
      onRequestConditionalFormat,
      createHistory,
      getSheetId,
      getSheetName,
      rowCount,
      columnCount,
      activeSheetId,
      onCreateConditionalFormattingRule,
      onUpdateConditionalFormattingRule,
      onDeleteConditionalFormattingRule,
      onPreviewConditionalFormattingRule,
  } = useSpreadsheetState()
  return (
    <>
      <CanvasGrid
        conditionalFormats={conditionalFormats}
        onRequestConditionalFormat={onRequestConditionalFormat}      
      />
  
      <ConditionalFormatDialog>
        <ConditionalFormatEditor
          sheetId={activeSheetId}
          rowCount={rowCount}
          columnCount={columnCount}
          getSheetName={getSheetName}
          getSheetId={getSheetId}
          theme={theme}
          conditionalFormats={conditionalFormats}
          onCreateRule={onCreateConditionalFormattingRule}
          onDeleteRule={onDeleteConditionalFormattingRule}
          onUpdateRule={onUpdateConditionalFormattingRule}
          onPreviewRule={onPreviewConditionalFormattingRule}
        />
      </ConditionalFormatDialog>
    </>
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);

```


# Sparklines

Inline mini-charts inside cells — line, column, or win/loss

Sparklines are mini-charts rendered inside a single cell. They're authored as `SPARKLINE()` formulas, which means they live alongside the rest of the calculation graph — referenced cells recompute the sparkline on change, copy/paste duplicates the formula, and XLSX + ODS round-trip preserves every option.

## Basic usage

Type `=SPARKLINE(A1:A10)` into a cell. The cell's effective value becomes a `sparkline` structured value, the renderer paints the chart, and the formula bar still shows the source.

```
A1:A10 = numeric values
B1     = =SPARKLINE(A1:A10)        // line sparkline (default)
B2     = =SPARKLINE(A1:A10, {"charttype","column"})
B3     = =SPARKLINE(A1:A10, {"charttype","winloss"})
```

## Options

`SPARKLINE` takes an optional second argument — a flat array of `"key", value` pairs in Excel array literal syntax (`{…}`). All options round-trip both directions through XLSX (`<x14:sparklineGroup>` attributes) and via SPARKLINE formula text.

### Chart type + color

```
=SPARKLINE(A1:A10, {"charttype","line"})
=SPARKLINE(A1:A10, {"charttype","column"})
=SPARKLINE(A1:A10, {"charttype","winloss"})
=SPARKLINE(A1:A10, {"color","#638ec6"})
```

### Axis options

```
// Render an X-axis line
=SPARKLINE(A1:A10, {"axis",TRUE})

// Plot empty cells as zero (default: gap)
=SPARKLINE(A1:A10, {"emptycells","zero"})

// Or connect across blanks
=SPARKLINE(A1:A10, {"emptycells","span"})

// Treat the X axis as a date axis (when paired with a date series)
=SPARKLINE(A1:A10, {"dateaxis",TRUE})
```

### Point highlights

```
=SPARKLINE(A1:A10, {
  "markers",TRUE,           // line sparkline: diamond markers at every point
  "highpoint",TRUE,         // highlight the maximum
  "lowpoint",TRUE,          // highlight the minimum
  "firstpoint",TRUE,        // highlight the first point
  "lastpoint",TRUE,         // highlight the last point
  "negativepoints",TRUE     // column/winloss: color negative bars differently
})
```

### Axis bounds

By default each sparkline scales to its own min/max ("individual"). Switch to a group-wide or custom scale:

```
// Use a custom max + min instead of auto
=SPARKLINE(A1:A10, {"max",100, "min",-10})

// Scale to the group of sparklines in the same x14:sparklineGroup
=SPARKLINE(A1:A10, {"minaxistype","group", "maxaxistype","group"})

// Custom — exact values from max/min
=SPARKLINE(A1:A10, {"minaxistype","custom", "maxaxistype","custom"})
```

### Line weight

```
=SPARKLINE(A1:A10, {"lineweight",2.5})    // line sparkline only
```

### Composing options

Combine any of the above in a single options block:

```
=SPARKLINE(A1:A10, {
  "charttype","line",
  "color","#1d4ed8",
  "markers",TRUE,
  "highpoint",TRUE,
  "lowpoint",TRUE,
  "lineweight",2,
  "emptycells","span"
})
```

## XLSX round-trip

Sparklines export as `<x14:sparklineGroup>` blocks inside the worksheet's `extLst`. Two sparklines share a group when every axis option matches; distinct settings each get their own group block. The exporter follows Excel's grouping rule precisely so a saved-and-reopened file produces the same UI.

The legacy default highlight behavior (markers + high + low for line; high + low + negative for column / winloss) ships unchanged for bare `SPARKLINE(range)` formulas without explicit options.

## ODS round-trip

LibreOffice's `calcext:sparkline-groups` extension is parsed on import and emitted on export.

## Working with structured values

A sparkline cell's `ev.structuredValue` is a `SparkLineResult`:

```ts
type SparkLineResult = {
  kind: "sparkline";
  data: number[];
  options: Record<string, string | number | boolean>;
};
```

Custom renderers can read this directly and replace the default chart implementation.


# Comments

Legacy single-author cell notes with author + rich text + author color

Spreadsheet supports Excel's **legacy comments** model — single-author notes with rich text + author metadata, anchored to a single cell. These map to OOXML `comments.xml` on round-trip.

Excel 2016+ **threaded comments** (with replies and resolved state, stored in `commentsThreaded.xml`) are not yet supported.

## Authoring a comment

A comment lives on `CellData.note` as plain text, or as a richer pointer via `commentThreadId` (reserved for the future threaded model). For the legacy single-author case, `note` is the field:

```ts
const cellData: CellData = {
  ue: { sv: "Q3 revenue" },
  fv: "Q3 revenue",
  note: "Includes the Atlas-2 contract — see slack #revenue for context.",
};
```

The renderer paints a small triangle marker in the cell's corner. Hovering shows the note.

## Round-trip

* **XLSX**: parsed from `xl/comments<N>.xml`, emitted on the way out. Author + creation timestamp preserved.
* **ODS**: parsed from `office:annotation` children of the cell node.

## Threading

Excel 2016 introduced threaded comments stored in `xl/threadedComments/threadedComment<N>.xml` — different file, different schema. These carry replies, resolved state, person IDs, and @mentions in the thread body.

Threading is currently a Tier-1 round-trip gap. Files that use threaded comments will:

* Import: the legacy comment shim (`comments.xml`) is still read; threaded replies + resolved state are dropped.
* Export: written as a flat legacy comment.

Track it on `docs/getting-started/excel-google-sheet-compatibility.md` under "Comments (threaded with replies)".

## Mentions in comments

For `@user` mentions inside notes / comments, see [Mentions](/configuration/features/mentions). Mentions work on the live cell editor; persistence into the threaded-comments OOXML store is part of the same gap above.


# Gradient cell fills

Linear and radial gradient fills on individual cells — round-trips XLSX and renders on canvas

Set a gradient on any cell's `uf` (user-entered format) and the canvas renderer paints it directly. Supports both **linear** (with a rotation angle) and **path / radial** (with axis-of-radial insets), each with two or more color stops.

Mirrors OOXML's `<gradientFill>` element. XLSX round-trip is full; the canvas paint uses native `CanvasGradient` (`createLinearGradient` / `createRadialGradient`).

## Authoring

```ts
const cellData: CellData = {
  ue: { sv: "Header" },
  fv: "Header",
  uf: {
    gradient: {
      type: "linear",
      degree: 90,              // 0 = left→right, 90 = top→bottom, 180 = right→left, 270 = bottom→top
      stops: [
        { position: 0, color: "#fde68a" },
        { position: 1, color: "#dc2626" },
      ],
    },
  },
};
```

`position` is on `[0, 1]`. At least two stops are required; three+ stops give multi-stop ramps.

## Path / radial gradients

Use `type: "path"` with four optional inset values describing where the inner stop's "core" rectangle sits relative to the cell:

```ts
uf: {
  gradient: {
    type: "path",
    left: 0.5,
    right: 0.5,
    top: 0.5,
    bottom: 0.5,
    stops: [
      { position: 0, color: "#ffffff" },   // center
      { position: 1, color: "#7c3aed" },   // edges
    ],
  },
}
```

The example above produces a radial gradient centered in the cell. Shift the center off-axis by adjusting `left` / `right` / `top` / `bottom` (each 0..1 — the inset of the inner stop's core from that edge).

## Three-stop ramps

```ts
uf: {
  gradient: {
    type: "linear",
    degree: 45,
    stops: [
      { position: 0,   color: "#fef08a" },   // yellow at one end
      { position: 0.5, color: "#22c55e" },   // green in the middle
      { position: 1,   color: "#1e40af" },   // blue at the other end
    ],
  },
}
```

## Precedence over solid + pattern fills

When `gradient` is present on a cell's format, it takes precedence over `backgroundColor` and `fillPattern`. Excel's UI treats fills as mutually exclusive on a single cell, so the renderer matches that: pattern + solid are ignored.

The `cellXfsRegistry` deduplication hash includes the gradient stops, so two cells with the same gradient share a `sid` reference (same compression behavior as for regular fills).

## XLSX round-trip

Parser reads `<gradientFill>` from `xl/styles.xml` and hydrates the `gradient` field on `CellFormat`. Exporter emits the same element with `<stop position><color rgb="..."/></stop>` children. Both linear and path/radial round-trip. The OOXML attributes map 1:1 onto the type's fields:

| OOXML                                   | Field                             |
| --------------------------------------- | --------------------------------- |
| `type="linear"` + `degree`              | `type: "linear"`, `degree`        |
| `type="path"` + `left/right/top/bottom` | `type: "path"`, four inset fields |
| `<stop position><color rgb=…/></stop>`  | `stops` array                     |

## Canvas rendering

The renderer constructs a `CanvasGradient` for each cell:

* **Linear**: the gradient axis projects through the cell's centroid at the given angle; start/end points are where that axis exits the bounding box.
* **Path / radial**: a `createRadialGradient` anchored at the inset midpoint with radius = distance to the farthest corner.

`setLineDash` and stroke styles are preserved — gradient cells still get cell borders, conditional-format icons, etc.

## Storybook example

See the `compatShowcaseSheet` in the storybook (Sheet 1, row 12) for a side-by-side gallery: `linear 0°`, `linear 90°`, `linear 45° (3 stops)`, `path (radial)`, `linear 180°`, and a `linear vert 3-stop` example. The pattern-fill row (row 11) is positioned right above so you can compare the two paint paths.


# Linked data types

Cells that carry a structured entity (Stocks, Geography, custom) with refreshable fields accessed via dot notation

Linked data types are Excel's "Stocks" and "Geography" cells generalized into a framework. A cell holds an **entity** with named fields, and other cells reference those fields via dot notation (`=A1.price`). The library ships the entity model + a provider plug-in API; you wire your own data source.

This is how the storybook's "Insert stock at A1" button works — it writes an `entity` structured value into a cell, then `refreshDataTypes` walks every entity cell and calls the matching provider to refresh fields.

## The entity shape

A linked-data-type cell's `ev.structuredValue` is an entity:

```ts
type Entity = {
  kind: "entity";
  dataType: string;          // matches a DataTypeProvider.id
  externalId: string;        // the ticker / location / etc.
  status: "loading" | "loaded" | "stale" | "error";
  fields?: Record<string, string | number | boolean>;
  formattedValue?: string;
};
```

The cell renderer paints `formattedValue` (or `fv`) as the display text. Field-access formulas (`=A1.price`) resolve through `getEffectiveStructuredValue`.

## Wiring a provider

```ts
import type { DataTypeProvider } from "@rowsncolumns/spreadsheet-state";

const stockProvider: DataTypeProvider = {
  id: "stock",
  schema: () => [
    { field: "name", type: "string" },
    { field: "price", type: "number", numberFormat: '"$"#,##0.00' },
    { field: "currency", type: "string" },
    { field: "changePercent", type: "number", numberFormat: "0.00%" },
  ],
  async refresh(entity) {
    const ticker = (entity.externalId ?? "").toUpperCase();
    const row = await fetchStockQuote(ticker);
    return {
      fields: {
        name: row.name,
        price: row.price,
        currency: row.currency,
        changePercent: row.changePercent,
      },
      formattedValue: `${ticker}  $${row.price.toFixed(2)}`,
    };
  },
};
```

Pass providers to `useSpreadsheetState`:

```ts
const { refreshDataTypes } = useSpreadsheetState({
  dataTypeProviders: [stockProvider],
  getEffectiveStructuredValue: (structuredValue, property) => {
    if (property && structuredValue?.kind === "entity") {
      return resolveEntityField(structuredValue, property);
    }
    if (property) {
      const value = get(structuredValue, property);
      return value ?? "";
    }
    return structuredValue;
  },
});
```

`resolveEntityField` is exported from `@rowsncolumns/spreadsheet-state`; it reads `entity.fields[property]`.

## Inserting a linked data type

Write the entity shape into the cell's `ev.structuredValue`:

```ts
const tickers = ["AAPL", "MSFT", "GOOG"];
onChangeSheetData?.((prev) => {
  const next = { ...prev };
  const sheet = (next[activeSheetId] ?? []).slice();
  next[activeSheetId] = sheet;
  tickers.forEach((ticker, idx) => {
    const rowIndex = 1 + idx;
    const row = sheet[rowIndex] ?? {};
    const values = (row.values ?? []).slice();
    values[1] = {
      ev: {
        structuredValue: {
          kind: "entity",
          dataType: "stock",
          externalId: ticker,
          status: "stale",
          formattedValue: ticker,
        },
      },
      fv: ticker,
    };
    sheet[rowIndex] = { ...row, values };
  });
  return next;
});

// Now refresh so fields hydrate
await refreshDataTypes?.({ sheetId: activeSheetId });
```

After the refresh:

* `=A2.price` returns the numeric price.
* `=A2.changePercent` returns the percent (with format applied).
* `=SUM(A2:A4.price)` aggregates across multiple linked-data-type cells.

## Dot-notation field access

`=cellRef.field` resolves through the formula engine's structured-value branch. The field name matches a key in `entity.fields`. The library auto-applies the `numberFormat` declared in the provider's `schema()` to the result.

```
A2 entity {ticker: "AAPL", price: 182.50, currency: "USD"}

=A2                  → "AAPL  $182.50"   (cell's formattedValue)
=A2.price            → 182.50            (numeric)
=A2.currency         → "USD"             (string)
=A2.price * 100      → 18250
```

## Refresh model

`refreshDataTypes({sheetId})` walks every cell whose `structuredValue.kind === "entity"` and calls the matching provider's `refresh()`. Each refresh sets the cell's `status` to `"loading"` while in flight, then `"loaded"` (or `"error"`) when complete. Cells with downstream `=A1.field` formulas recompute automatically once the entity lands.

`status: "stale"` is the initial state for a freshly-inserted entity and triggers a refresh on the next `refreshDataTypes` call.

## What's not yet supported

* **OOXML `linkedDataType` round-trip** — Excel's native Stocks / Geography panels emit a specific element that's not yet parsed or emitted. Custom provider entities survive the export as plain JSON in `structuredValue` if the host writes them as such; Excel's native Stocks won't be recognized.

See [Excel compatibility](/getting-started/excel-google-sheet-compatibility) → "Phase 12 — linked data types" for the deferral notes.


# LAMBDA and higher-order functions

LAMBDA, LET, MAP/REDUCE/BYROW/BYCOL/SCAN/MAKEARRAY, and GROUPBY/PIVOTBY

The formula engine ships Excel's full LAMBDA family: `LAMBDA` as both an IIFE and a value, `LET` for named bindings, the six standard higher-order functions (`MAP`, `REDUCE`, `BYROW`, `BYCOL`, `SCAN`, `MAKEARRAY`), and `GROUPBY` / `PIVOTBY` built on the same infrastructure. Dependency tracking through lambda bodies is automatic.

## LAMBDA

Two shapes — both work:

**IIFE** (call inline):

```
=LAMBDA(x, x*x)(5)         → 25
=LAMBDA(a, b, a+b)(2, 3)   → 5
```

**As a value** (pass to a HOF or store on a named range):

```
=MAP(A1:A3, LAMBDA(v, v*2))
```

Internally a standalone `LAMBDA(...)` rewrites at compile time to `__RNC_LAMBDA__("params", "body source")`, which constructs a runtime lambda value `{__isLambda, params, body}`. HOFs unwrap that value and re-evaluate the body once per element with bindings substituted in.

A recursion guard caps depth at 256 to keep runaway lambdas from blowing the JS stack.

## LET

Bind names to values inside a formula. Source-level expansion: each binding's value is inlined into the body before parse.

```
=LET(n, 5, n*2)                  → 10
=LET(a, 3, b, 4, a+b)            → 7
=LET(name, "World", "Hello "&name) → "Hello World"
```

Nested LET works:

```
=LET(x, 10, LET(y, x*2, y+1))    → 21
```

A LET-bound LAMBDA can be called like a function — the engine substitutes Function-shaped occurrences of binding names with the lambda source, then the IIFE inliner collapses it:

```
=LET(double, LAMBDA(x, x*2), double(5))    → 10
=LET(add, LAMBDA(a, b, a+b), add(3, 4))    → 7
```

## Closures via LET capture

LET expands BEFORE LAMBDA, so any LET binding referenced inside a LAMBDA body is baked into the body source. This gives you closures for free:

```
=LET(n, 5, LAMBDA(x, x+n))
// becomes: __RNC_LAMBDA__("x", "x+(5)")
```

```
=LET(n, 10, LET(addn, LAMBDA(x, x+n), addn(5)))   → 15
```

## Higher-order functions

### MAP

Apply a lambda to each element of an array, returns a same-shape result:

```
=MAP(A1:A3, LAMBDA(v, v*2))
// [[1], [2], [3]] → [[2], [4], [6]]
```

Multiple input arrays of the same shape:

```
=MAP(A1:A3, B1:B3, LAMBDA(a, b, a+b))
```

### REDUCE

Left fold:

```
=REDUCE(0, A1:A3, LAMBDA(acc, v, acc+v))    // sum
=REDUCE(1, A1:A3, LAMBDA(acc, v, acc*v))    // product
```

### SCAN

Like REDUCE but returns intermediate accumulator values, same shape as input:

```
=SCAN(0, A1:A3, LAMBDA(acc, v, acc+v))      // running total
// [[1], [2], [3]] → [[1], [3], [6]]
```

### BYROW / BYCOL

Apply a lambda to each row (or column) as a 1D array:

```
=BYROW(A1:C3, LAMBDA(row, SUM(row)))
=BYCOL(A1:C3, LAMBDA(col, AVERAGE(col)))
```

### MAKEARRAY

Build an N×M array by invoking a lambda with `(r, c)` per cell (1-indexed per Excel):

```
=MAKEARRAY(2, 3, LAMBDA(r, c, r*10+c))
// [[11, 12, 13],
//  [21, 22, 23]]
```

## GROUPBY

Aggregate rows by row-field tuple, calling a lambda once per group on the group's value vector:

```
=GROUPBY(A1:A6, B1:B6, LAMBDA(v, SUM(v)))
```

Args:

| Arg               | Purpose                                                                           |
| ----------------- | --------------------------------------------------------------------------------- |
| `row_fields`      | column or 2D — each row defines a group key tuple                                 |
| `values`          | column or 2D — values to aggregate per group                                      |
| `function`        | LAMBDA receiving the group's value vector (1D array) and returning a scalar       |
| `[field_headers]` | 0 (default) / 1 — echo the leading header row in output                           |
| `[total_depth]`   | 0 (default) / 1 grand-total at top / -1 grand-total at bottom                     |
| `[sort_order]`    | 1 (default, asc) / -1 (desc) by the first row-field column                        |
| `[filter_array]`  | optional boolean column same length as row\_fields; rows where FALSE are excluded |

```
// Sum per category
=GROUPBY(A1:A6, B1:B6, LAMBDA(v, SUM(v)))

// Average per category, descending
=GROUPBY(A1:A6, B1:B6, LAMBDA(v, AVERAGE(v)), 0, 0, -1)

// With grand total at top
=GROUPBY(A1:A6, B1:B6, LAMBDA(v, SUM(v)), 0, 1)

// Count + filter
=GROUPBY(A1:A6, B1:B6, LAMBDA(v, COUNT(v)), 0, 0, 1, C1:C6)
```

The aggregation lambda receives the group's value vector as an Excel array literal (`{a, b, c}`) substituted into the body source — so any aggregator that accepts an array works (`SUM`, `AVERAGE`, `COUNT`, `MAX`, `MIN`, custom LAMBDAs, etc.).

## PIVOTBY

Two-axis cousin of GROUPBY — adds a column axis on top of the row grouping:

```
=PIVOTBY(rows, cols, values, function,
         [row_headers], [row_total_depth], [row_sort_order],
         [col_headers], [col_total_depth], [col_sort_order],
         [filter_array])
```

```
// Rows = regions, columns = quarters, values summed
=PIVOTBY(A1:A8, B1:B8, C1:C8, LAMBDA(v, SUM(v)))

// With row + column grand totals at top/left
=PIVOTBY(A1:A8, B1:B8, C1:C8, LAMBDA(v, SUM(v)), 0, 1, 1, 0, 1, 1)

// Totals at bottom/right
=PIVOTBY(A1:A8, B1:B8, C1:C8, LAMBDA(v, SUM(v)), 0, -1, 1, 0, -1, 1)

// Sort both axes descending
=PIVOTBY(A1:A8, B1:B8, C1:C8, LAMBDA(v, SUM(v)), 0, 0, -1, 0, 0, -1)
```

Output is a 2D spilled array — a header row of distinct column-tuples, then one row per distinct row-tuple prefixed by row labels.

## Dependency tracking through lambda bodies

When a lambda body references a cell, the dep parser walks the body string via a fresh inner parser instance and registers each ref as a precedent of the outer formula. Recalc propagates correctly:

```
=MAP(A1:A3, LAMBDA(v, v+B1))
// Both A1:A3 AND B1 are tracked as dependencies
```

```
=REDUCE(0, A1:A3, LAMBDA(acc, v, acc+v+C5))
// C5 is tracked
```

## Named LAMBDAs (workbook scope)

Define a named range whose value is a LAMBDA and call it like a function:

```
// In Name Manager:
Commission = =LAMBDA(sales, IF(sales > 1000, sales*0.1, sales*0.05))

// Anywhere in the workbook:
=Commission(500)    → 25
=Commission(5000)   → 500
=MAP(A1:A10, Commission)
```

See [Named ranges](/configuration/features/named-ranges) for the full story on workbook + sheet scoping.

## What's not yet supported

**Recursive LAMBDA** — `=LET(fact, LAMBDA(n, IF(n<=1, 1, n*fact(n-1))), fact(5))` doesn't work yet. Body-reparse at runtime loses the LET binding for `fact`. A runtime registry that survives per-call re-parse is the fix; deferred.

See `docs/getting-started/excel-google-sheet-compatibility.md` → "Phase 7" for the full status.


# Undo/Redo

All change history is preserved. Making versioning simple.

Change histories are stored as `immer` patches. But developers can choose their own state management modules, hence libraries like Redux or MobX can create state histories/patches.

`useSpreadsheetState` uses `immer` to product patches during state updates.

```tsx
import React, { useState } from "react";
import { CanvasGrid, CellData } from "@rowsncolumns/spreadsheet";
import { produceWithPatches } from "immer";
import {
  SheetData,
  useSpreadsheetState,
} from "@rowsncolumns/spreadsheet-state";

const MySpreadsheet = () => {
  const [sheetData, onChangeSheetData] = useState<SheetData<CellData>>({});
  const { createHistory } = useSpreadsheetState({});
  return (
    <CanvasGrid
      sheetId={1}
      rowCount={100}
      columnCount={100}
      onChange={(sheetId, activeCell, value) => {
        const saveHistory = createHistory();
        // Update sheet data
        onChangeSheetData?.((prevSheetData) => {
          const [nextState, patches, inversePatches] = produceWithPatches(
            prevSheetData,
            (draft) => {
              draft[sheetId][activeCell.rowIndex].values[
                activeCell.columnIndex
              ] = {
                ue: { sv: value },
              };
            }
          );

          saveHistory({
            sheetData: { patches, inversePatches },
          });

          return nextState;
        });
      }}
    />
  );
};
```


# Components


# Canvas Grid

The Canvas Grid that renders the spreadsheet

CanvasGrid is a controlled component that accepts a set of props and exposes callbacks for developers to hook into.

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";

const App = () => {
  return (
    <SpreadsheetProvider>
      <CanvasGrid
        rowCount={1000}
        columnCount={1000}
        sheetId={1}
      />
    </SpreadsheetProvider>
  );
};
```

## CanvasGrid options

The following options are supported by CanvasGrid component

```typescript
export type CanvasGridProps<T extends CellData = CellData> = {
  licenseKey?: string;
  /**
   * ID of the selected sheet
   */
  sheetId: number;
  /**
   * Initial active cell of the sheet
   */
  activeCell?: CellInterface;
  /**
   * Initial cell selections of the sheet
   */
  selections?: SelectionArea<SelectionAttributes>[];
  /**
   * Spreadsheet theme
   */
  theme?: SpreadsheetTheme;
  /**
   * Automatically focus on the Spreadsheet
   */
  autoFocus?: boolean;
  /**
   * Frozen row count
   */
  frozenRowCount?: number;
  /**
   * Frozen column count
   */
  frozenColumnCount?: number;
  /**
   * Total no of rows
   */
  rowCount?: number;
  /**
   * Total no of columns
   */
  columnCount?: number;
  /**
   * Scaling factor
   */
  scale?: number;
  /**
   * @deprecated basicFilter. Use tables instead
   */
  basicFilter?: FilterView;
  /**
   * Filter views
   */
  tables?: TableView[];
  /**
   * Named ranges
   */
  namedRanges?: NamedRange[];
  /**
   * Banded ranges
   */
  bandedRanges?: BandedRange[];
  /**
   * Collaborators of the sheetsActivecell + Selections of the
   * collaborator will be highlighted. Exclude current user
   */
  users?: Collaborator[];
  /**
   * Current user's userId
   */
  userId?: number | string;
  /**
   * Color of filter view border
   */
  tableBorderColor?: string;
  /**
   * Color of the filter icon
   */
  filterIconColor?: string;
  /**
   * Show filter icon in header cells
   */
  showFilterIcon?: boolean;
  /**
   * Show calculated column icon
   */
  showCalculatedColumnIcon?: boolean;
  /**
   * Conditional formatting
   */
  conditionalFormats?: ConditionalFormatRule[];
  /**
   * Embedded chart
   */
  charts?: EmbeddedChart[];
  /**
   * Pivot tables
   */
  pivotTables?: PivotTable[];
  /**
   * Embed any object on top of a sheet
   */
  embeds?: EmbeddedObject[];
  /**
   * Default row height
   */
  defaultRowHeight?: number;
  /**
   * Default column width
   */
  defaultColumnWidth?: number;
  /**
   * Default column header height
   */
  defaultColumnHeaderHeight?: number;
  /**
   * Default row header width
   */
  defaultRowHeaderWidth?: number;
  /**
   * Color of the sheet grid lines
   */
  gridLineColor?: string;
  /**
   * Default text color
   */
  cellColor?: string;
  /**
   * Header color of banded cell
   */
  bandedHeaderColor?: string;
  /**
   * Opacity of first band
   */
  bandedOpacity?: number;
  /**
   * Default background color of a cell
   */
  cellBackgroundColor?: string;
  /**
   * Border color of header cell
   */
  headerBorderColor?: string;
  /**
   * Background color of header cells
   */
  headerBackgroundColor?: string;
  /**
   * Header text color
   */
  headerColor?: string;
  /**
   * When a cell is selected
   */
  headerActiveBackgroundColor?: string;
  /**
   * Header background when an entire row or column is selected
   */
  headerSelectedBackgroundColor?: string;
  /**
   * Text color of header when an entire row or column is selected
   */
  headerSelectedColor?: string;
  /**
   * Header background for table cells
   */
  headerTableBackgroundColor?: string;
  /**
   * Header background when table cell is selected
   */
  headerTableActiveBackgroundColor?: string;
  /**
   * When an entire header row or column is selected
   */
  headerTableSelectedBackgroundColor?: string;
  /**
   * Selected border color for charts
   */
  chartSelectedColor?: string;
  /**
   * Background color of chips
   */
  chipBackgroundColor?: string;
  /**
   * Shadow color of frozen row
   */
  frozenShadowColor?: string;
  /**
   * Thickness of frozen shadow
   */
  frozenShadowThickness?: number;
  /**
   * Border color of selections
   */
  selectionBorderColor?: string;
  /**
   * Background selection color
   */
  selectionBackgroundColor?: string;
  /**
   * Border color when selection is dragged
   */
  selectionDragBorderColor?: string;
  /**
   * Merged cells
   */
  merges?: GridRange[];
  /**
   * Protected cell ranges
   */
  protectedRanges?: ProtectedRange[];
  /**
   * Additional bordered areas in the grid
   */
  borderStyles?: RangeBorderStyle[];
  /**
   * Meta data of a row
   */
  rowMetadata?: Sheet["columnMetadata"];
  /**
   * Meta data of a column
   */
  columnMetadata?: Sheet["columnMetadata"];
  /**
   * Enable sticky editor
   */
  stickyEditor?: boolean;
  /**
   * Scrollbar size
   */
  scrollbarSize?: number;
  /**
   * Show grid lines
   */
  showGridLines?: boolean;
  /**
   * Show frozen line separator for row and column
   */
  showFrozenSeparator?: boolean;
  /**
   * Show formulas in cells
   */
  showFormulas?: boolean;
  /**
   * Show spreadsheet row and column headers
   */
  showHeaders?: boolean;
  /**
   * Snaps to row or column while scrolling
   * Recommended for 60fps scrolling, Since canvas
   * does not need to redraw at each scroll frame
   */
  scrollSnap?: boolean;
  /**
   * How fast scroll should snap to row or column
   */
  scrollThrottleTimeout?: number;
  /**
   * Cell selection policy
   */
  selectionPolicy?: UseSelectionOptions["selectionPolicy"];
  /**
   * Enable enter key to edit a cell
   * When false, pressing enter will select the next cell
   */
  enableEditOnEnterKey?: boolean;
  /**
   * Show header menu button that invokes context menu
   */
  showHeaderMenuButton?: boolean;
  /**
   * Enable text overflow to neighbouring empty cells
   * Enabling it will have an performance impact
   */
  enableTextOverflow?: boolean;
  /**
   * Number of additional columns to
   * render on the left and right
   * Useful, when text overflows adjacent cells
   */
  overscanCount?: number;
  /**
   * Header renderer
   */
  HeaderCell?: ElementType<HeaderCellProps>;
  /**
   * Cell renderer
   */
  Cell?: ElementType<CellProps>;
  /**
   * Selection title component
   */
  SelectionTitleComponent?: ElementType<SelectionTitleProps>;
  /**
   * Cell Editor
   */
  CellEditor?: ElementType;
  /**
   * Tooltip
   */
  CellTooltip?: ElementType<CellTooltipProps>;
  /**
   * Custom Context menu component
   */
  ContextMenu?: ElementType;
  /**
   * Filterbox component
   */
  FilterBox?: ElementType | ReactElement;
  /**
   * Paste menu component, shown after a paste operation
   */
  PasteMenu?: React.ComponentType<PasteMenuProps>;
  /**
   * Agent suggestion action component
   */
  SuggestionAction?: React.ComponentType<SuggestionActionProps>;
  /**
   * Hidden row markers, shown in the row header for hidden rows
   */
  HiddenRowMarkers?: React.ComponentType<HiddenRowMarkersProps>;
  /**
   * Hidden column markers, shown in the column header for hidden columns
   */
  HiddenColumnMarkers?: React.ComponentType<HiddenColumnMarkersProps>;
  /**
   * Chart component
   */
  getChartComponent?(props: ChartComponentProps): React.ReactElement;
  /**
   * Chart component
   */
  getEmbedComponent?(props: EmbedComponentProps): React.ReactElement;
  /**
   * User scrolls the grid
   * @param scrollCoords
   */
  onScroll?(scrollCoords: ScrollCoords): void;
  /**
   * View port changes
   */
  onViewPortChange?(viewport: ViewPortProps): void;
  /**
   * Callback when user changes a cell value
   * @param sheetId
   * @param cell
   * @param value
   */
  onChange?(
    sheetId: number,
    cell: CellInterface,
    value: string | boolean,
    previousValue: string | boolean | undefined,
    isDirty?: boolean
  ): void;
  /**
   * User is deleting a cell or selections
   * @param sheetId
   * @param cell
   * @param selections
   */
  onDelete?(
    sheetId: number,
    cell: CellInterface,
    selections: SelectionArea<SelectionAttributes>[]
  ): void;
  /**
   * When user changes editor value
   * @param value
   * @param sheetId
   * @param cell
   */
  onChangeEditorValue?(
    value: string,
    sheetId: number,
    cell: CellInterface
  ): void;
  /**
   * For result preview panel in cell editor
   * @param value
   * @param sheetId
   * @param cell
   *
   * @returns FormulaResult
   */
  onRequestCalculate?(
    value: string,
    sheetId: number,
    rowIndex: number,
    columnIndex: number,
    isArray?: boolean
  ): Promise<
    | undefined
    | string
    | number
    | boolean
    | Date
    | (string | number | boolean | Date)[][]
  >;
  /**
   * Fill
   * @param cell
   * @param selections
   */
  onFill?(
    sheetId: number,
    cell: CellInterface,
    fillSelection: SelectionArea<SelectionAttributes>,
    selections: SelectionArea<SelectionAttributes>[]
  ): void;
  /**
   * Callback when user moves a selection
   * @param from
   * @param to
   */
  onMoveSelection?(
    sheetId: number,
    from: SelectionArea<SelectionAttributes>,
    to: SelectionArea<SelectionAttributes>
  ): void;
  /**
   * Callback when active cell changes
   */
  onChangeActiveCell?(cell: CellInterface): void;
  /**
   * Callback when selections cell changes
   */
  onChangeSelections?(
    selections: SelectionArea<SelectionAttributes>[],
    finishedSelection?: boolean
  ): void;
  /**
   * Formulas can trigger sheet change
   */
  onChangeActiveSheet?(sheetId: number): void;
  /**
   * Get data of a specific cell
   * @param rowIndex
   * @param columnIndex
   * @returns CellData
   */
  getCellData?(
    sheetId: number,
    rowIndex: number,
    columnIndex: number
  ): T | null | undefined;
  /**
   * Callback when user resizes a column or row
   * @param dimension
   * @param index
   * @param axis
   * @param autoFit
   */
  onResize?(
    sheetId: number,
    indexes: number[],
    dimension: number,
    axis: AXIS,
    autoFit?: boolean
  ): void;
  /**
   * Hide column
   * @param columnIndexes
   */
  onHideColumn?(sheetId: number, columnIndexes: number[]): void;
  /**
   * Hide row
   * @param rowIndexes
   */
  onHideRow?(sheetId: number, rowIndexes: number[]): void;
  /**
   * Show column
   * @param columnIndex
   */
  onShowColumn?(sheetId: number, columnIndexes: number[]): void;
  /**
   * Show row
   * @param rowIndex
   */
  onShowRow?(sheetId: number, rowIndexes: number[]): void;
  /**
   * Delete column
   * @param columnIndex
   */
  onDeleteColumn?(sheetId: number, columnIndexes: number[]): void;
  /**
   * Delete row
   * @param rowIndex
   */
  onDeleteRow?(sheetId: number, rowIndexes: number[]): void;
  /**
   * Insert column
   * @param columnIndex
   */
  onInsertColumn?(sheetId: number, columnIndex: number): void;
  /**
   * Insert row
   * @param rowIndex
   */
  onInsertRow?(sheetId: number, rowIndex: number): void;
  /**
   * When user inserts cell and shift down
   * @param cell
   */
  onInsertCellsShiftDown?(sheetId: number, cell: CellInterface): void;
  /**
   * When user inserts cell and shift right
   * @param cell
   */
  onInsertCellsShiftRight?(sheetId: number, cell: CellInterface): void;
  /**
   * Delete cells and shift left
   * @param cell
   */
  onDeleteCellsShiftLeft?(sheetId: number, cell: CellInterface): void;
  /**
   * Delete cells and shift up
   * @param cell
   */
  onDeleteCellsShiftUp?(sheetId: number, cell: CellInterface): void;
  /**
   * Callback when columns are moved
   * @param dims
   * @param toColumn
   */
  onMoveColumns?(sheetId: number, dims: number[], toColumn: number): void;
  /**
   * Callback when rows are moved
   * @param dims
   * @param toRow
   */
  onMoveRows?(sheetId: number, dims: number[], toRow: number): void;
  /**
   * User pressed a key to select previous sheet
   */
  onSelectPreviousSheet?(e?: React.KeyboardEvent<HTMLDivElement>): void;
  /**
   * User pressed a key to select previous sheet
   */
  onSelectNextSheet?(e?: React.KeyboardEvent<HTMLDivElement>): void;
  /**
   * Shift + Fn + F11
   * To insert a new sheet
   */
  onCreateNewSheet?(): void;
  /**
   * Keydown handler
   */
  onKeyDown?(e: React.KeyboardEvent<HTMLDivElement>): void;
  /**
   * When user changes formatting, Eg: bold, underline
   */
  onChangeFormatting?<T extends FormattingType>(
    sheetId: number,
    type: T,
    value: FormattingValue<T>
  ): void;
  /**
   * When user presses F4, we can replay the last formatting
   * on a new cell
   * @param sheetId
   */
  onRepeatFormatting?(sheetId: number): void;
  /**
   * User is trying to clear all formatting from
   * cells
   */
  onClearFormatting?(sheetId: number): void;
  /**
   * Callback when user clears contents
   */
  onClearContents?(sheetId: number, activeCell: CellInterface): void;
  /**
   * Called when user tries to edit a protected cell
   * @param cell
   */
  onEditProtectedCell?(sheetId: number, cell: CellInterface): void;
  /**
   * Return sheet name from sheetId
   * @param sheetId
   */
  getSheetName?(sheetId: number): string;
  /**
   * Callback when user resizes a chart
   * @param width
   * @param height
   */
  onResizeChart?(
    chartId: number,
    anchorCell: CellInterface,
    offsetXPixels: number,
    offsetYPixels: number,
    width: number,
    height: number
  ): void;
  /**
   * User is trying to delete an embed
   * @param chartId
   */
  onDeleteChart?(chartId: number): void;
  /**
   * Fired when user moves a chart
   * @param anchorCell
   * @param offsetXPixels
   * @param offsetYPixels
   */
  onMoveChart?(
    chartId: number,
    anchorCell: CellInterface,
    offsetXPixels: number,
    offsetYPixels: number
  ): void;
  /**
   * Callback when user resizes an embed
   * @param width
   * @param height
   */
  onResizeEmbed?(
    embedId: number,
    anchorCell: CellInterface,
    offsetXPixels: number,
    offsetYPixels: number,
    width: number,
    height: number
  ): void;
  /**
   * User is trying to delete an embed
   * @param embedId
   */
  onDeleteEmbed?(embedId: number): void;
  /**
   * Fired when user moves an embed
   * @param anchorCell
   * @param offsetXPixels
   * @param offsetYPixels
   */
  onMoveEmbed?(
    embedId: number,
    anchorCell: CellInterface,
    offsetXPixels: number,
    offsetYPixels: number
  ): void;
  /**
   * When user tries to fill range
   * @param activeCell
   * @param selections
   * @param direction
   *
   * Command + Enter => Fill range
   * Command + D => Fill down
   * Command + R => Fill right
   */
  onFillRange?(
    sheetId: number,
    activeCell: CellInterface,
    selections: SelectionArea<SelectionAttributes>[],
    direction?: Direction
  ): void;
  /**
   * On insert current time
   * @param activeCell
   * @param selections
   */
  onInsertTime?(
    sheetId: number,
    activeCell: CellInterface,
    selections: SelectionArea<SelectionAttributes>[]
  ): void;
  /**
   * On insert date
   * @param activeCell
   * @param selections
   */
  onInsertDate?(
    sheetId: number,
    activeCell: CellInterface,
    selections: SelectionArea<SelectionAttributes>[]
  ): void;
  /**
   * Freeze columns
   * @param sheetId
   * @param columnIndex
   */
  onFreezeColumn?(sheetId: number, columnIndex: number): void;
  /**
   * Freeze rows
   * @param sheetId
   * @param columnIndex
   */
  onFreezeRow?(sheetId: number, rowIndex: number): void;
  /**
   * User tries to undo an action
   */
  onUndo?(): void;
  /**
   * User tries to redo an action
   */
  onRedo?(): void;
  /**
   * Callback when user paste a set of cells
   * @param e Browser event
   * @param copiedSelections Selection that user copied
   * @param activeCell Current active cell
   * @param selections Current active selections
   * @param shouldDeleteSource If user has cut a selection, we should delete copiedSelections
   */
  onPaste?(
    e: ClipboardEvent | undefined,
    sourceSheetId: number | undefined,
    destinationSheetId: number,
    copiedSelections: SelectionArea<SelectionAttributes>[] | undefined,
    activeCell: CellInterface,
    selections: SelectionArea<SelectionAttributes>[] | undefined,
    finalSelections: SelectionArea<SelectionAttributes>[] | undefined,
    shouldDeleteSource: boolean
  ): void;
  /**
   * Callback when user copies a selection
   * useful to save copied selection till user
   * paste's the selection
   * @param e
   * @param copiedSelections Selections that user copied
   */
  onCopy?(
    e: ClipboardEvent | undefined,
    sheetId: number,
    copiedSelections: SelectionArea<SelectionAttributes>[]
  ): void;
  /**
   * Callback when user cuts a selection
   * @param e
   */
  onCut?(e: ClipboardEvent | undefined, sheetId: number): void;
  /**
   * Sort sheet by columnIndex
   * @param columnIndex
   * @param sortOrder
   */
  onSortColumn?(
    sheetId: number,
    columnIndex: number,
    sortOrder: SortOrder
  ): void;
  /**
   * Callback when user sorts a basicFilter or Table
   */
  onSortTable?(
    sheetId: number,
    filter: FilterView | TableView,
    columnIndex: number,
    direction: SortOrder
  ): void;
  /**
   * Callback when applies a filter
   * @param columnIndex
   * @param conditionType
   * @param conditionValue
   * @param hiddenValues
   */
  onFilterTable?(
    sheetId: number,
    filter: FilterView | TableView,
    columnIndex: number,
    conditionType: ConditionType | undefined,
    conditionValue: ConditionValue[] | undefined,
    hiddenValues: string[]
  ): void;
  /**
   * Callback when user resizes a table
   * @param sheetId
   * @param tableId
   * @param range
   */
  onResizeTable?(sheetId: number, table: TableView, range: GridRange): void;
  /**
   * Callback when user updates or adds a new note
   * @param sheetId
   * @param cell
   * @param notes
   */
  onUpdateNote?(sheetId: number, cell: CellInterface, notes?: string): void;
  /**
   * User wants to protect a range of cells or sheet
   * @param sheetId
   * @param cell
   * @param selections
   */
  onProtectRange?(
    sheetId: number,
    cell: CellInterface,
    selections: SelectionArea<SelectionAttributes>[] | undefined
  ): void;
  /**
   * Un protect range by Id
   * @param sheetId
   * @param protectedRangeId
   */
  onUnProtectRange?(sheetId: number, protectedRangeId: number): void;
  /**
   * Sort a selection range
   * @param sheetId
   * @param selection
   */
  onSortRange?(
    sheetId: number,
    selection: SelectionArea<SelectionAttributes>[],
    sortOrder: SortOrder
  ): void;
  /**
   * On Request edit a table
   * Developers can choose to show a modal dialog
   * @param table
   */
  onRequestEditTable?(table: TableView): void;
  /**
   * Handle keyboard shortcut to create a new table
   * Option + Command + T
   * Ctrl + Option + T
   */
  onCreateTable?(
    sheetId: number,
    activeCell: CellInterface,
    selections: SelectionArea<SelectionAttributes>[],
    headerRow?: boolean
  ): void;
  /**
   * Request to edit a pivot table configuration
   * @param pivotId
   */
  onRequestEditPivotTable?(pivotId: string): void;
  /**
   * Request to delete a pivot table
   * @param pivotId
   */
  onRequestDeletePivotTable?(pivotId: string): void;
  /**
   * Define a new or edit an existing named range
   */
  onRequestDefineNamedRange?(
    sheetId: number,
    activeCell: CellInterface,
    selections: SelectionArea<SelectionAttributes>[]
  ): void;
  /**
   * Get text for a row header
   * @param rowIndex
   */
  getRowHeaderText?(rowIndex: number): string;
  /**
   * Get text for a column header
   * @param columnIndex
   */
  getColumnHeaderText?(columnIndex: number): string;
  /**
   * Parses a text value to return tokens
   * @param value
   */
  tokenizer?(value: string | undefined, sheetName?: string): ParsedToken[];
  /**
   * All functions supported by the
   * calculator, used for suggestions
   * in the formula dropdown
   */
  functionDescriptions?: CalculatorFunction[];
};
```


# Toolbar

Toolbar component is a wrapper for all formatting buttons of the Spreadsheet

Toolbar component can be imported from spreadsheet module using the following code. You can compose your own toolbar or even use a third-party component to render the buttons.

[Radix icons](https://icons.radix-ui.com/) are used for the toolbar buttons, but this can be easily swapped to any icon set by using custom components.

<figure><img src="/files/KRZNH75lgVWUy3YrKya9" alt=""><figcaption><p>Toolbar</p></figcaption></figure>

<pre class="language-tsx"><code class="lang-tsx"><strong>import {
</strong>  Toolbar,
  ButtonDecreaseDecimal,
  ButtonFormatCurrency,
  ButtonFormatPercent,
  ButtonIncreaseDecimal,
  ButtonRedo,
  ButtonUndo,
  ScaleSelector,
  ToolbarSeparator,
  BackgroundColorSelector,
  BorderSelector,
  ButtonBold,
  ButtonItalic,
  ButtonStrikethrough,
  ButtonUnderline,
  ButtonInsertImage,
  ButtonInsertLink,
  ButtonSwitchColorMode,
  ThemeSelector,
  TableActions,
  FontFamilySelector,
  FontSizeSelector,
  MergeCellsSelector,
  TextColorSelector,
  TextFormatSelector,
  TextHorizontalAlignSelector,
  TextVerticalAlignSelector,
  TextWrapSelector
} from "@rowsncolumns/spreadsheet";
import { Separator } from "@rowsncolumns/ui";

const App = () => {
  return (
    &#x3C;Toolbar>
      &#x3C;ButtonUndo />
      &#x3C;ButtonRedo />
      &#x3C;ToolbarSeparator />
      &#x3C;ScaleSelector />
      &#x3C;ToolbarSeparator />
      &#x3C;ButtonFormatCurrency />
      &#x3C;ButtonFormatPercent />
      &#x3C;ButtonDecreaseDecimal />
      &#x3C;ButtonIncreaseDecimal />
      &#x3C;TextFormatSelector />
      &#x3C;ToolbarSeparator />
      &#x3C;FontFamilySelector />
      &#x3C;ToolbarSeparator />
      &#x3C;FontSizeSelector />
      &#x3C;ToolbarSeparator />
      &#x3C;ButtonBold />
      &#x3C;ButtonItalic />
      &#x3C;ButtonUnderline />
      &#x3C;ButtonStrikethrough />
      &#x3C;TextColorSelector />
      &#x3C;ToolbarSeparator />
      &#x3C;BackgroundColorSelector />
      &#x3C;BorderSelector />
      &#x3C;MergeCellsSelector />
      &#x3C;ToolbarSeparator />
      &#x3C;TextHorizontalAlignSelector />
      &#x3C;TextVerticalAlignSelector />
      &#x3C;TextWrapSelector />
      
      &#x3C;ButtonInsertImage />
      &#x3C;ButtonInsertLink />
      &#x3C;ButtonSwitchColorMode />
      &#x3C;ThemeSelector />
      &#x3C;TableAction />
    &#x3C;/Toolbar>
  );
};
</code></pre>

## BottomBar

BottomBar component is a container for the footer of the Spreadsheet, which houses Sheet Tabs, Sheet Switcher, Sheet Status and NewSheetButton component


# Formula Bar

Display and edit cell formulas and values

The Formula Bar is a compound component that displays the active cell reference and allows users to view and edit cell values and formulas. It consists of three main sub-components: `FormulaBar`, `FormulaBarLabel`, and `FormulaBarInput`.

## Overview

The Formula Bar provides:

* **Cell reference display**: Shows the active cell address (e.g., "A1", "Sheet2!B5")
* **Formula editing**: Edit cell formulas and values in a larger input area
* **Formula preview**: View complete formulas without cell width constraints
* **Function hints**: Display function syntax and autocomplete

## Components

### FormulaBar

The parent container that wraps the formula bar components.

```tsx
import { FormulaBar, FormulaBarLabel, FormulaBarInput } from "@rowsncolumns/spreadsheet";

<FormulaBar>
  <FormulaBarLabel>A1</FormulaBarLabel>
  <FormulaBarInput
    value="=SUM(A1:A10)"
    onChange={handleChange}
  />
</FormulaBar>
```

### FormulaBarLabel

Displays the active cell reference or range.

```tsx
import { FormulaBarLabel } from "@rowsncolumns/spreadsheet";
import { cellToAddress } from "@rowsncolumns/utils";

function MyFormulaBar() {
  const { activeCell, activeSheetId, selections } = useSpreadsheetState({});

  const label = selections.length > 1
    ? selectionToAddress(selections[0])
    : cellToAddress(activeCell);

  return (
    <FormulaBarLabel>
      {label}
    </FormulaBarLabel>
  );
}
```

### FormulaBarInput

Input field for editing cell values and formulas.

```tsx
import { FormulaBarInput } from "@rowsncolumns/spreadsheet";

<FormulaBarInput
  value={cellValue}
  onChange={(value) => onChange(activeSheetId, activeCell, value)}
  onKeyDown={handleKeyDown}
  functionDescriptions={functionDescriptions}
/>
```

## Complete Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  FormulaBar,
  FormulaBarLabel,
  FormulaBarInput,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
} from "@rowsncolumns/spreadsheet-state";
import { cellToAddress, selectionToAddress } from "@rowsncolumns/utils";
import { functionDescriptions } from "@rowsncolumns/functions";

function SpreadsheetWithFormulaBar() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet 1" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    getSheetName,
    onChangeActiveCell,
    onChangeSelections,
    onChange,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  // Get current cell data
  const cellData = getCellData(
    activeSheetId,
    activeCell.rowIndex,
    activeCell.columnIndex
  );

  // Determine label (cell reference or range)
  const formulaBarLabel = selections.length > 0 && selections[0]
    ? selectionToAddress(selections[0])
    : cellToAddress(activeCell);

  // Get cell value (formula or formatted value)
  const cellValue = cellData?.ue?.fv
    || cellData?.fv
    || "";

  return (
    <SpreadsheetProvider>
      <div className="flex flex-col h-screen">
        {/* Formula Bar */}
        <FormulaBar>
          <FormulaBarLabel>{formulaBarLabel}</FormulaBarLabel>
          <FormulaBarInput
            value={cellValue}
            onChange={(value) => {
              onChange?.(
                activeSheetId,
                activeCell,
                value,
                cellData?.fv
              );
            }}
            functionDescriptions={functionDescriptions}
            getSheetName={getSheetName}
          />
        </FormulaBar>

        {/* Spreadsheet Grid */}
        <div className="flex-1">
          <CanvasGrid
            sheetId={activeSheetId}
            activeCell={activeCell}
            selections={selections}
            getCellData={getCellData}
            onChangeActiveCell={onChangeActiveCell}
            onChangeSelections={onChangeSelections}
            onChange={onChange}
            functionDescriptions={functionDescriptions}
          />
        </div>
      </div>
    </SpreadsheetProvider>
  );
}

export default SpreadsheetWithFormulaBar;
```

## Props

### FormulaBar

| Prop        | Type              | Description                                         |
| ----------- | ----------------- | --------------------------------------------------- |
| `children`  | `React.ReactNode` | Child components (FormulaBarLabel, FormulaBarInput) |
| `className` | `string`          | Optional CSS class name                             |

### FormulaBarLabel

| Prop        | Type              | Description                               |
| ----------- | ----------------- | ----------------------------------------- |
| `children`  | `React.ReactNode` | Cell reference text (e.g., "A1", "B2:D5") |
| `className` | `string`          | Optional CSS class name                   |

### FormulaBarInput

| Prop                   | Type                               | Description                           |
| ---------------------- | ---------------------------------- | ------------------------------------- |
| `value`                | `string`                           | Current cell value or formula         |
| `onChange`             | `(value: string) => void`          | Callback when value changes           |
| `onKeyDown`            | `(e: React.KeyboardEvent) => void` | Optional keyboard handler             |
| `functionDescriptions` | `CalculatorFunction[]`             | Function definitions for autocomplete |
| `getSheetName`         | `(sheetId: number) => string`      | Get sheet name by ID                  |
| `className`            | `string`                           | Optional CSS class name               |

## Features

### Formula Autocomplete

The FormulaBarInput supports autocomplete for functions when `functionDescriptions` is provided:

```tsx
import { functionDescriptions, functions } from "@rowsncolumns/functions";

<FormulaBarInput
  value={cellValue}
  onChange={onChange}
  functionDescriptions={functionDescriptions}
/>
```

When users type `=SUM(`, the input shows function hints and parameter information.

### Multi-Cell Selection Display

When multiple cells are selected, the label shows the range:

```tsx
const label = selections.length > 0 && selections[0]
  ? selectionToAddress(selections[0])  // "A1:E10"
  : cellToAddress(activeCell);         // "A1"

<FormulaBarLabel>{label}</FormulaBarLabel>
```

### Named Range Display

Show named range when applicable:

```tsx
const getFormulaBarLabel = () => {
  // Check if selection is a named range
  const selection = selections[0];
  const namedRange = namedRanges.find(nr =>
    rangesEqual(nr.range, selection?.range)
  );

  if (namedRange) {
    return namedRange.name; // "SalesData"
  }

  return selectionToAddress(selection) || cellToAddress(activeCell);
};
```

### Cross-Sheet References

Display sheet names in cross-sheet references:

```tsx
import { getSheetName } from "@rowsncolumns/spreadsheet-state";

const label = activeSheetId !== 1
  ? `${getSheetName(activeSheetId)}!${cellToAddress(activeCell)}`
  : cellToAddress(activeCell);

<FormulaBarLabel>{label}</FormulaBarLabel>
```

## Keyboard Shortcuts

The Formula Bar supports standard keyboard shortcuts:

* **Enter**: Confirm and move to next cell
* **Escape**: Cancel editing
* **Tab**: Autocomplete function name
* **F2**: Edit cell in formula bar
* **Ctrl/Cmd + A**: Select all text

## Styling

The Formula Bar can be styled using CSS classes:

```tsx
<FormulaBar className="border-b bg-white">
  <FormulaBarLabel className="font-mono text-sm px-3 py-2 border-r bg-gray-50" />
  <FormulaBarInput className="flex-1 px-3 py-2 font-mono text-sm" />
</FormulaBar>
```

## Integration with Toolbar

Combine with toolbar for a complete spreadsheet interface:

```tsx
import { Toolbar, FormulaBar } from "@rowsncolumns/spreadsheet";

<div className="flex flex-col h-screen">
  <Toolbar>
    {/* Toolbar buttons */}
  </Toolbar>

  <FormulaBar>
    <FormulaBarLabel>{label}</FormulaBarLabel>
    <FormulaBarInput value={value} onChange={onChange} />
  </FormulaBar>

  <div className="flex-1">
    <CanvasGrid {...props} />
  </div>
</div>
```

## Advanced Usage

### Custom Formula Validation

Validate formulas before applying:

```tsx
const handleFormulaChange = (value: string) => {
  if (value.startsWith("=")) {
    // Validate formula syntax
    try {
      validateFormula(value);
      onChange(activeSheetId, activeCell, value);
    } catch (error) {
      console.error("Invalid formula:", error);
    }
  } else {
    onChange(activeSheetId, activeCell, value);
  }
};

<FormulaBarInput
  value={cellValue}
  onChange={handleFormulaChange}
/>
```

### Formula Highlighting

Highlight cell references in formulas:

```tsx
const highlightedFormula = useMemo(() => {
  if (!cellValue.startsWith("=")) return cellValue;

  // Highlight cell references (A1, B2:C5, etc.)
  return cellValue.replace(
    /([A-Z]+[0-9]+)(:[A-Z]+[0-9]+)?/g,
    '<span class="text-blue-600 font-semibold">$&</span>'
  );
}, [cellValue]);
```

### Read-Only Mode

Disable editing in read-only mode:

```tsx
<FormulaBarInput
  value={cellValue}
  onChange={readonly ? undefined : onChange}
  readOnly={readonly}
/>
```

## Best Practices

1. **Always Show Cell Reference**: Display the current cell or range reference in FormulaBarLabel
2. **Sync with Active Cell**: Update formula bar when active cell changes
3. **Handle Long Formulas**: Ensure the input can scroll for long formulas
4. **Provide Function Hints**: Include functionDescriptions for better UX
5. **Validate Input**: Check for formula errors before applying changes
6. **Support Keyboard Navigation**: Implement standard spreadsheet keyboard shortcuts

## Troubleshooting

### Formula Bar Not Updating

* Verify activeCell and selections are properly synced
* Check that getCellData returns the correct cell data
* Ensure onChange is called when cell values change

### Autocomplete Not Working

* Confirm functionDescriptions prop is provided
* Check that function definitions are properly formatted
* Verify the formula starts with "="

### Performance Issues

* Memoize cellValue calculation for large spreadsheets
* Use debouncing for onChange handler
* Consider lazy loading function descriptions

## See Also

* [Formula Input](/configuration/components/formula-input) - Standalone formula input component
* [Toolbar](/configuration/components/toolbar) - Toolbar component with formatting buttons
* [Canvas Grid](/configuration/components/canvas-grid) - Main spreadsheet grid component


# Font and Text Selectors

Font family, size, and text formatting selectors

A collection of toolbar components for font and text formatting including font family selection, font size adjustment, text alignment, and text wrapping options.

## Components Overview

* **FontFamilySelector**: Choose font family (Arial, Times New Roman, etc.)
* **FontSizeSelector**: Select font size in points
* **TextHorizontalAlignSelector**: Horizontal text alignment
* **TextVerticalAlignSelector**: Vertical text alignment
* **TextWrapSelector**: Text wrapping options
* **TextFormatSelector**: Additional text formatting options

## FontFamilySelector

Select the font family for cells.

### Basic Usage

```tsx
import { FontFamilySelector } from "@rowsncolumns/spreadsheet";

<FontFamilySelector
  value={currentCellFormat?.textFormat?.fontFamily}
  onChange={(value) => {
    onChangeFormatting(
      activeSheetId,
      activeCell,
      selections,
      "textFormat",
      { fontFamily: value }
    );
  }}
/>
```

### Complete Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Toolbar,
  FontFamilySelector,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
} from "@rowsncolumns/spreadsheet-state";

function SpreadsheetWithFontSelector() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet 1" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getEffectiveFormat,
    onChangeFormatting,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  const currentCellFormat = getEffectiveFormat(
    activeSheetId,
    activeCell.rowIndex,
    activeCell.columnIndex
  );

  return (
    <SpreadsheetProvider>
      <Toolbar>
        <FontFamilySelector
          value={currentCellFormat?.textFormat?.fontFamily}
          onChange={(value) => {
            onChangeFormatting(
              activeSheetId,
              activeCell,
              selections,
              "textFormat",
              { fontFamily: value }
            );
          }}
        />
      </Toolbar>

      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

### Props

| Prop        | Type                      | Description                                            |
| ----------- | ------------------------- | ------------------------------------------------------ |
| `value`     | `string`                  | Current font family (e.g., "Arial", "Times New Roman") |
| `onChange`  | `(value: string) => void` | Callback when font family changes                      |
| `className` | `string`                  | Optional CSS class name                                |

### Supported Fonts

Default supported fonts include:

* Arial
* Times New Roman
* Courier New
* Georgia
* Verdana
* Comic Sans MS
* Impact
* Trebuchet MS

## FontSizeSelector

Select font size in points.

### Basic Usage

```tsx
import { FontSizeSelector, DEFAULT_FONT_SIZE_PT } from "@rowsncolumns/spreadsheet";

<FontSizeSelector
  value={currentCellFormat?.textFormat?.fontSize ?? DEFAULT_FONT_SIZE_PT}
  onChange={(value) => {
    onChangeFormatting(
      activeSheetId,
      activeCell,
      selections,
      "textFormat",
      { fontSize: value }
    );
  }}
/>
```

### Props

| Prop        | Type                      | Description                               |
| ----------- | ------------------------- | ----------------------------------------- |
| `value`     | `number`                  | Current font size in points (default: 11) |
| `onChange`  | `(value: number) => void` | Callback when font size changes           |
| `className` | `string`                  | Optional CSS class name                   |

### Common Font Sizes

* 8pt (Very Small)
* 10pt (Small)
* 11pt (Default)
* 12pt (Normal)
* 14pt (Large)
* 18pt (Heading)
* 24pt (Large Heading)
* 36pt (Display)

## TextHorizontalAlignSelector

Horizontal text alignment selector.

### Basic Usage

```tsx
import { TextHorizontalAlignSelector } from "@rowsncolumns/spreadsheet";

<TextHorizontalAlignSelector
  value={currentCellFormat?.horizontalAlignment}
  onChange={(value) => {
    onChangeFormatting(
      activeSheetId,
      activeCell,
      selections,
      "horizontalAlignment",
      value
    );
  }}
/>
```

### Props

| Prop        | Type                            | Description                     |
| ----------- | ------------------------------- | ------------------------------- |
| `value`     | `"left" \| "center" \| "right"` | Current horizontal alignment    |
| `onChange`  | `(value) => void`               | Callback when alignment changes |
| `className` | `string`                        | Optional CSS class name         |

### Alignment Options

* **Left**: Align text to the left (default for text)
* **Center**: Center text horizontally
* **Right**: Align text to the right (default for numbers)

## TextVerticalAlignSelector

Vertical text alignment selector.

### Basic Usage

```tsx
import { TextVerticalAlignSelector } from "@rowsncolumns/spreadsheet";

<TextVerticalAlignSelector
  value={currentCellFormat?.verticalAlignment}
  onChange={(value) => {
    onChangeFormatting(
      activeSheetId,
      activeCell,
      selections,
      "verticalAlignment",
      value
    );
  }}
/>
```

### Props

| Prop        | Type                            | Description                     |
| ----------- | ------------------------------- | ------------------------------- |
| `value`     | `"top" \| "middle" \| "bottom"` | Current vertical alignment      |
| `onChange`  | `(value) => void`               | Callback when alignment changes |
| `className` | `string`                        | Optional CSS class name         |

### Alignment Options

* **Top**: Align text to top of cell
* **Middle**: Center text vertically (default)
* **Bottom**: Align text to bottom of cell

## TextWrapSelector

Text wrapping options.

### Basic Usage

```tsx
import { TextWrapSelector } from "@rowsncolumns/spreadsheet";

<TextWrapSelector
  value={currentCellFormat?.wrapStrategy}
  onChange={(value) => {
    onChangeFormatting(
      activeSheetId,
      activeCell,
      selections,
      "wrapStrategy",
      value
    );
  }}
/>
```

### Props

| Prop        | Type                             | Description                         |
| ----------- | -------------------------------- | ----------------------------------- |
| `value`     | `"overflow" \| "clip" \| "wrap"` | Current wrap strategy               |
| `onChange`  | `(value) => void`                | Callback when wrap strategy changes |
| `className` | `string`                         | Optional CSS class name             |

### Wrap Options

* **Overflow**: Text overflows into adjacent cells (default)
* **Clip**: Text is clipped at cell boundary
* **Wrap**: Text wraps within the cell

## TextFormatSelector

Additional text formatting options.

### Basic Usage

```tsx
import { TextFormatSelector } from "@rowsncolumns/spreadsheet";

<TextFormatSelector
  value={currentCellFormat?.textRotation}
  onChange={(value) => {
    onChangeFormatting(
      activeSheetId,
      activeCell,
      selections,
      "textRotation",
      value
    );
  }}
/>
```

### Props

| Prop        | Type                      | Description                             |
| ----------- | ------------------------- | --------------------------------------- |
| `value`     | `number`                  | Text rotation angle (-90 to 90 degrees) |
| `onChange`  | `(value: number) => void` | Callback when rotation changes          |
| `className` | `string`                  | Optional CSS class name                 |

## Complete Toolbar Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Toolbar,
  ToolbarSeparator,
  FontFamilySelector,
  FontSizeSelector,
  TextHorizontalAlignSelector,
  TextVerticalAlignSelector,
  TextWrapSelector,
  ButtonBold,
  ButtonItalic,
  ButtonUnderline,
  DEFAULT_FONT_SIZE_PT,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
} from "@rowsncolumns/spreadsheet-state";

function SpreadsheetWithTextFormatting() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet 1" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    getEffectiveFormat,
    onChangeActiveCell,
    onChangeSelections,
    onChangeFormatting,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  const currentCellFormat = getEffectiveFormat(
    activeSheetId,
    activeCell.rowIndex,
    activeCell.columnIndex
  );

  return (
    <SpreadsheetProvider>
      <div className="flex flex-col h-screen">
        <Toolbar>
          {/* Font Family */}
          <FontFamilySelector
            value={currentCellFormat?.textFormat?.fontFamily}
            onChange={(value) => {
              onChangeFormatting(
                activeSheetId,
                activeCell,
                selections,
                "textFormat",
                { fontFamily: value }
              );
            }}
          />

          {/* Font Size */}
          <FontSizeSelector
            value={currentCellFormat?.textFormat?.fontSize ?? DEFAULT_FONT_SIZE_PT}
            onChange={(value) => {
              onChangeFormatting(
                activeSheetId,
                activeCell,
                selections,
                "textFormat",
                { fontSize: value }
              );
            }}
          />

          <ToolbarSeparator />

          {/* Bold, Italic, Underline */}
          <ButtonBold
            active={currentCellFormat?.textFormat?.bold}
            onClick={() => {
              onChangeFormatting(
                activeSheetId,
                activeCell,
                selections,
                "textFormat",
                { bold: !currentCellFormat?.textFormat?.bold }
              );
            }}
          />

          <ButtonItalic
            active={currentCellFormat?.textFormat?.italic}
            onClick={() => {
              onChangeFormatting(
                activeSheetId,
                activeCell,
                selections,
                "textFormat",
                { italic: !currentCellFormat?.textFormat?.italic }
              );
            }}
          />

          <ButtonUnderline
            active={currentCellFormat?.textFormat?.underline}
            onClick={() => {
              onChangeFormatting(
                activeSheetId,
                activeCell,
                selections,
                "textFormat",
                { underline: !currentCellFormat?.textFormat?.underline }
              );
            }}
          />

          <ToolbarSeparator />

          {/* Text Alignment */}
          <TextHorizontalAlignSelector
            value={currentCellFormat?.horizontalAlignment}
            onChange={(value) => {
              onChangeFormatting(
                activeSheetId,
                activeCell,
                selections,
                "horizontalAlignment",
                value
              );
            }}
          />

          <TextVerticalAlignSelector
            value={currentCellFormat?.verticalAlignment}
            onChange={(value) => {
              onChangeFormatting(
                activeSheetId,
                activeCell,
                selections,
                "verticalAlignment",
                value
              );
            }}
          />

          {/* Text Wrapping */}
          <TextWrapSelector
            value={currentCellFormat?.wrapStrategy}
            onChange={(value) => {
              onChangeFormatting(
                activeSheetId,
                activeCell,
                selections,
                "wrapStrategy",
                value
              );
            }}
          />
        </Toolbar>

        <div className="flex-1">
          <CanvasGrid
            sheetId={activeSheetId}
            activeCell={activeCell}
            selections={selections}
            getCellData={getCellData}
            onChangeActiveCell={onChangeActiveCell}
            onChangeSelections={onChangeSelections}
          />
        </div>
      </div>
    </SpreadsheetProvider>
  );
}

export default SpreadsheetWithTextFormatting;
```

## Keyboard Shortcuts

Common keyboard shortcuts for text formatting:

* **Ctrl/Cmd + B**: Toggle bold
* **Ctrl/Cmd + I**: Toggle italic
* **Ctrl/Cmd + U**: Toggle underline
* **Ctrl/Cmd + L**: Align left
* **Ctrl/Cmd + E**: Align center
* **Ctrl/Cmd + R**: Align right

## Styling

Customize selector appearance with CSS:

```tsx
<FontFamilySelector
  className="min-w-32 border rounded"
  value={fontFamily}
  onChange={onChange}
/>

<FontSizeSelector
  className="w-16"
  value={fontSize}
  onChange={onChange}
/>
```

## Best Practices

1. **Show Current Format**: Always display the current cell's formatting state
2. **Multi-Selection Support**: Apply formatting to all selected cells
3. **Use getEffectiveFormat**: Get the computed format including defaults
4. **Provide Visual Feedback**: Highlight active formatting buttons
5. **Group Related Controls**: Use ToolbarSeparator to organize formatting options

## Use Cases

### Custom Font Lists

Provide custom font families:

```tsx
const customFonts = [
  "Roboto",
  "Open Sans",
  "Lato",
  "Montserrat",
  "Raleway",
];

<FontFamilySelector
  fonts={customFonts}
  value={fontFamily}
  onChange={onChange}
/>
```

### Font Size Presets

Common font size presets:

```tsx
const fontSizePresets = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36];

<FontSizeSelector
  presets={fontSizePresets}
  value={fontSize}
  onChange={onChange}
/>
```

### Conditional Formatting

Apply different alignments based on data type:

```tsx
const handleAlignmentChange = (value) => {
  const cellData = getCellData(activeSheetId, activeCell.rowIndex, activeCell.columnIndex);

  // Auto-align numbers to the right
  if (typeof cellData?.ev?.nv === "number") {
    value = "right";
  }

  onChangeFormatting(activeSheetId, activeCell, selections, "horizontalAlignment", value);
};
```

## Troubleshooting

### Selectors Not Showing Current Value

* Ensure getEffectiveFormat returns the correct cell format
* Check that activeCell and selections are properly synced
* Verify formatting data is stored correctly in sheetData

### Font Changes Not Applying

* Confirm onChangeFormatting is called with correct parameters
* Check that the font family name matches exactly
* Ensure the font is loaded (use loadWebFont for custom fonts)

### Performance Issues

* Memoize currentCellFormat calculation
* Debounce onChange handlers for font size input
* Use React.memo for selector components

## See Also

* [Toolbar](/configuration/components/toolbar) - Main toolbar component
* [Text Color Selector](/configuration/components/toolbar#textcolorselector) - Text color picker
* [Background Color Selector](/configuration/components/toolbar#backgroundcolorselector) - Background color picker
* [Border Selector](/configuration/components/toolbar#borderselector) - Cell border formatting


# Sheet Tabs

Quickly switch between multiple sheets

Optional SheetTab component to select sheets, and change sheet attributes. You can choose to use your preferred Tab component

<figure><img src="/files/1Pzul4gWqSqRkBSVSgKZ" alt=""><figcaption><p>Sheet tabs</p></figcaption></figure>

```tsx
import { SheetTabs } from "@rowsncolumns/spreadsheet"

const App = () => {
  return (
    <SheetTabs
      sheets={sheets}
      activeSheetId={activeSheetId}
      onChangeActiveSheet={onChangeActiveSheet}
      onRenameSheet={onRenameSheet}
      onChangeSheetTabColor={onChangeSheetTabColor}
      onDeleteSheet={onRequestDeleteSheet}
      onHideSheet={onHideSheet}
      onMoveSheet={onMoveSheet}
      onProtectSheet={onProtectSheet}
      onUnProtectSheet={onUnProtectSheet}
      onDuplicateSheet={onDuplicateSheet}
    />
  )
}
```

## Custom context menu

Each sheet tab can display a custom context menu. Use the `ContextMenu` props to inject a react component

You can use the default menu as a template to build your own Context menu

<https://github.com/rowsncolumns/spreadsheet/blob/main/apps/spreadsheet/components/sheet-tabs/context-menu.tsx>

Contextmenu uses shadcn/radix-ui dropdown components internally.

```tsx
import type { SheetTabContextMenuProps } from "@rowsncolumns/spreadsheet"
import {
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuPortal,
  DropdownMenuSeparator,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownRightSlot,
} from "@rowsncolumns/ui";

const CustomContextMenu = ({ onDeleteSheet, canDelete, sheetId }: SheetTabContextMenuProps) => {
  return (
    <DropdownMenuPortal>
      <DropdownMenuContent align="start">
        <DropdownMenuItem
          onClick={() => onDeleteSheet?.(sheetId)}
          disabled={!canDelete}
        >
          Delete
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenuPortal>
  )
}
<SheetTabs ContextMenu={CustomContextMenu}>
```


# Sheet Switcher

Dropdown menu with a list of sheets, both hidden and visible

<figure><img src="/files/DZ6rD4lL2bgCsYQ5FoAg" alt=""><figcaption><p>Sheet Switcher</p></figcaption></figure>

```tsx
import { SheetSwitcher } from "@rowsncolumns/spreadsheet"

const App = () => {
  return (
    <SheetSwitcher
      sheets={sheets}
      activeSheetId={activeSheetId}
      onChangeActiveSheet={onChangeActiveSheet}
      onShowSheet={onShowSheet}
    />
  )
}
```


# Sheet Status

Show average, count and sum of numerical values

SheetStatus uses the calculator to find Average, Count and Sum value of selected numerical fields

<figure><img src="/files/soXLJzwjJpLNZQiZ9CzF" alt=""><figcaption><p>SheetStatus component</p></figcaption></figure>

```tsx
import { SheetStatus } from "@rowsncolumns/spreadsheet"

const App = () => {
  return (
    <SheetStatus
      sheetId={activeSheetId}
      activeCell={activeCell}
      selections={selections}
      onRequestCalculate={onRequestCalculate}
      rowCount={rowCount}
      columnCount={columnCount}
    />
  )
}
```


# Range Selector

Displays and let users quickly select a named range

RangeSelector displays the currently selected cell address and creates a dropdown menu to select named ranges and sheets.

<figure><img src="/files/9K6AGPBDtkOTE0yOEO51" alt=""><figcaption><p>Range Selector</p></figcaption></figure>

```tsx
import { RangeSelector } from "@rowsncolumns/spreadsheet"

const App = () => {
  return (
    <RangeSelector
      selections={selections}
      activeCell={activeCell}
      onChangeActiveCell={onChangeActiveCell}
      onChangeSelections={onChangeSelections}
      sheets={sheets}
      rowCount={rowCount}
      columnCount={columnCount}
      onChangeActiveSheet={onChangeActiveSheet}
      namedRanges={namedRanges}
    />
  )
}
```


# Formula Input

Use a formula input component outside the spreadsheet

Formula input component allows users to enter formulas, make cell selections from Spreadsheet.

<figure><img src="/files/ej8POatfrgXsqeadbGlz" alt=""><figcaption><p>Formula Input component</p></figcaption></figure>

It can be placed anywhere inside the SpreadsheetProvider and use it part of your custom interface.

```tsx
import { FormulaInput } from "@rowsncolumns/spreadsheet"

const App = () => {
  const [ value, onChange ] = useState("")
  return (
    <FormulaInput
      value={value}
      onChange={onChange}
    />
  )
}
```


# Selection Input

Get selections from Spreadsheet and display it in an input field

<figure><img src="/files/JV7EVO5CLz4mt9jvniTU" alt=""><figcaption><p>Display selections and let users make selection input</p></figcaption></figure>

```tsx
import { SelectionInput } from "@rowsncolumns/spreadsheet"

const App = () => {
  const [ value, onChange ] = useState("")
  return (
    <SelectionInput
      onChange={onChange}
      value={value}
      selections={selections}
      activeCell={activeCell}
    />
  )
}
```


# SheetSearch

Search within a sheet with this component

<figure><img src="/files/DIStnUGlacVU4PMSpbjc" alt=""><figcaption><p>Ctrl +F to display Sheet Search component</p></figcaption></figure>

{% code overflow="wrap" %}

```tsx
import { CanvasGrid, SheetSearch } from "@rowsncolumns/spreadsheet"
import { useSpreadsheetState, useSearch } from "@rowsncolumns/spreadsheet-state"

const App = () => {
  const activeSheetId = 1
  const { getCellData, getNonEmptyColumnCount, getNonEmptyRowCount } = useSpreadsheetState({
    ...
  })
  const {
      onSearch,
      onResetSearch,
      onFocusNextResult,
      onFocusPreviousResult,
      hasNextResult,
      hasPreviousResult,
      borderStyles,
      isSearchActive,
      onRequestSearch,
      totalResults,
      currentResult,
      searchQuery,
    } = useSearch({
      getCellData,
      sheetId: activeSheetId,
      getNonEmptyColumnCount,
      getNonEmptyRowCount,
    });
  return (
    <>
      <CanvasGrid
        borderStyles={borderStyles}
      />
      <SheetSearch
        isActive={isSearchActive}
        onSubmit={onSearch}
        onReset={onResetSearch}
        onNext={onFocusNextResult}
        onPrevious={onFocusPreviousResult}
        disableNext={!hasNextResult}
        disablePrevious={!hasPreviousResult}
        currentResult={currentResult}
        totalResults={totalResults}
        searchQuery={searchQuery}
      />
    </>
  )
}
```

{% endcode %}


# NamedRangeEditor

Create and edit named ranges

<figure><img src="/files/S3e0XSKoS4KDEDZXILXr" alt=""><figcaption><p>Create or edit named ranges</p></figcaption></figure>

<pre class="language-tsx" data-overflow="wrap"><code class="lang-tsx">import { CanvasGrid, NamedRangeEditor } from "@rowsncolumns/spreadsheet"
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state"

const App = () => {
  const activeSheetId = 1
<strong>  const { getSheetName, onCreateNamedRange, onUpdateNamedRange } = useSpreadsheetState({
</strong>    ...
  })
  return (
    &#x3C;>
      &#x3C;CanvasGrid
      />
      &#x3C;NamedRangeEditor
        sheetId={activeSheetId}
        rowCount={rowCount}
        columnCount={columnCount}
        getSheetName={getSheetName}
        onCreateNamedRange={onCreateNamedRange}
        onUpdateNamedRange={onUpdateNamedRange}
      />
    &#x3C;/>
  )
}
</code></pre>


# DeleteSheetConfirmation

A confirmation dialog that can be displayed before deleting a sheet

<figure><img src="/files/D71PVGOzQEaJp8IrbJaT" alt=""><figcaption><p>Delete sheet confirmation</p></figcaption></figure>

<pre class="language-tsx" data-overflow="wrap"><code class="lang-tsx">import { CanvasGrid, DeleteSheetConfirmation } from "@rowsncolumns/spreadsheet"
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state"

const App = () => {
  const activeSheetId = 1
<strong>  const { onDeleteSheet, onRequestDeleteSheet } = useSpreadsheetState({
</strong>    ...
  })
  return (
    &#x3C;>
      &#x3C;button onClick={onRequestDeleteSheet}>Delete current sheet&#x3C;/button>
      &#x3C;CanvasGrid />
      &#x3C;DeleteSheetConfirmation
        sheetId={activeSheetId}
        onDeleteSheet={onDeleteSheet}
      />
    &#x3C;/>
  )
}
</code></pre>


# TableEditor

A table editor dialog to edit name, range and other options of a table

<figure><img src="/files/f6jBdIwkioi8QpseqndR" alt=""><figcaption><p>Brings up a table editor</p></figcaption></figure>

<pre class="language-tsx" data-overflow="wrap"><code class="lang-tsx">import { CanvasGrid, DeleteSheetConfirmation } from "@rowsncolumns/spreadsheet"
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state"

const App = () => {
  const activeSheetId = 1
<strong>  const { onUpdateTable, onRequestEditTable, getSheetName } = useSpreadsheetState({
</strong>    ...
  })
  return (
    &#x3C;>
      &#x3C;button onClick={() => {
        onRequestEditTable(table)
      }>Edit table&#x3C;/button>
      &#x3C;CanvasGrid />
      &#x3C;TableEditor
        sheetId={activeSheetId}
        rowCount={rowCount}
        columnCount={columnCount}
        getSheetName={getSheetName}
        onSubmit={onUpdateTable}
      />
    &#x3C;/>
  )
}
</code></pre>


# Cell Format Editor

Component to edit cell format. It can be shown in a modal dialog or embedded in your own component.

<figure><img src="/files/WaYgvq8tpXUID1J82VSC" alt=""><figcaption><p>Cmd + Option + 1 will bring up the cell format editor</p></figcaption></figure>

<pre class="language-tsx" data-overflow="wrap"><code class="lang-tsx">import { CanvasGrid, defaultSpreadsheetTheme } from "@rowsncolumns/spreadsheet"
import { useSpreadsheetState, CellFormatEditor, CellFormatEditorDialog } from "@rowsncolumns/spreadsheet-state"

const App = () => {
  const activeSheetId = 1
  const [theme, onChangeTheme] = useState&#x3C;SpreadsheetTheme>(
    defaultSpreadsheetTheme
  );
<strong>  const {
</strong><strong>    activeCell,
</strong><strong>    selections,
</strong><strong>    theme,
</strong><strong>    onMergeCells,
</strong><strong>    onChangeFormatting,
</strong><strong>    onChangeBorder,
</strong><strong>    onRequestFormatCells,
</strong><strong>    getUserEnteredFormat,
</strong><strong>    getEffectiveValue
</strong><strong>  } = useSpreadsheetState({
</strong>    ...
  })
  
  const currentCellFormat = useMemo(
    () =>
      getUserEnteredFormat(
        activeSheetId,
        activeCell.rowIndex,
        activeCell.columnIndex
      ),
    [activeSheetId, activeCell, getUserEnteredFormat]
  );
  return (
    &#x3C;>
      &#x3C;CanvasGrid
        // Binds to keyboard shortcut and context menu
        onRequestFormatCells={onRequestFormatCells}
      />
      &#x3C;CellFormatEditorDialog>
        &#x3C;CellFormatEditor
          sheetId={activeSheetId}
          activeCell={activeCell}
          selections={selections}
          onChangeFormatting={onChangeFormatting}
          cellFormat={currentCellFormat}
          getEffectiveValue={getEffectiveValue}
          onMergeCells={onMergeCells}
          theme={theme}
          onChangeBorder={onChangeBorder}
        />
      &#x3C;/CellFormatEditorDialog>

    &#x3C;/>
  )
}
</code></pre>


# Conditional Format Editor

Adds a Conditonal format editor component

<figure><img src="/files/R9FzzPpJp3fwWmc8AogL" alt=""><figcaption><p>Conditional Format Editor</p></figcaption></figure>

{% code overflow="wrap" %}

```tsx
import { CanvasGrid, SpreadsheetProvider, defaultSpreadsheetTheme } from "@rowsncolumns/spreadsheet"
import { useSpreadsheet, ConditionalFormatDialog, ConditionalFormatEditor } from "@rowsncolumns/spreadsheet-state"

const Spreadsheet = () => {
  const [conditionalFormats, onChangeConditionalFormats] = useState<
      ConditionalFormatRule[]>([]);
  const [theme, onChangeTheme] = useState<SpreadsheetTheme>(
    defaultSpreadsheetTheme
  );
  const {
    activeSheetId,
    rowCount,
    columnCount,
    getSheetName,
    getSheetId,
    onCreateConditionalFormattingRule,
    onDeleteConditionalFormattingRule,
    onUpdateConditionalFormattingRule,
    onPreviewConditionalFormattingRule
  } = useSpreadsheetState({
    conditionalFormats
  })
  
  return (
    <>
      <ConditionalFormatDialog>
        <ConditionalFormatEditor
          sheetId={activeSheetId}
          rowCount={rowCount}
          columnCount={columnCount}
          theme={theme}
          conditionalFormats={conditionalFormats}
          onCreateRule={onCreateConditionalFormattingRule}
          onDeleteRule={onDeleteConditionalFormattingRule}
          onUpdateRule={onUpdateConditionalFormattingRule}
          onPreviewRule={onPreviewConditionalFormattingRule}
        />
      </ConditionalFormatDialog>
    </>
  )
}

const App = () => (
  <SpreadsheetProvider>
    <Spreadsheet />
  </SpreadsheetProvider>
)
```

{% endcode %}


# Data Validation Editor

Allow users to edit data validation rules

<figure><img src="/files/LWMb0tFNfqIK5IvqP5GB" alt=""><figcaption><p>Data validations editor screenshot</p></figcaption></figure>

{% code overflow="wrap" %}

```tsx
import { CanvasGrid, SpreadsheetProvider, defaultSpreadsheetTheme } from "@rowsncolumns/spreadsheet"
import { useSpreadsheet, ConditionalFormatDialog, ConditionalFormatEditor } from "@rowsncolumns/spreadsheet-state"

const Spreadsheet = () => {
  const [ dataValidations, onChangeDataValidations] = useState<
      DataValidationRuleRecord[]>([]);
      
  const {
    activeSheetId,
    rowCount,
    columnCount,
    onRequestDataValidation,
    onCreateDataValidationRule,
    onUpdateDataValidationRule,
    onDeleteDataValidationRule,
    onDeleteDataValidationRules
  } = useSpreadsheetState({
    dataValidations,
    onChangeDataValidations
  })
  
  return (
    <>
      <CanvasGrid
        // User is requesting to open editor
        onRequestDataValidation={onRequestDataValidation}
      />
      <DataValidationEditorDialog>
        <DataValidationEditor
          dataValidations={dataValidations}
          sheetId={activeSheetId}
          rowCount={rowCount}
          columnCount={columnCount}
          onDeleteRules={onDeleteDataValidationRules}
          onDeleteRule={onDeleteDataValidationRule}
          onCreateRule={onCreateDataValidationRule}
          onUpdateRule={onUpdateDataValidationRule}
        />
      </DataValidationEditorDialog>
    </>
  )
}

const App = () => (
  <SpreadsheetProvider>
    <Spreadsheet />
  </SpreadsheetProvider>
)
```

{% endcode %}


# Insert Link Editor

Add and remove hyperlinks in cells

<figure><img src="/files/y0qJnzxGEekNkLMLqDti" alt=""><figcaption><p>Insert Link editor</p></figcaption></figure>

## Inserting Links

{% code overflow="wrap" %}

```tsx
import { CanvasGrid, SpreadsheetProvider } from "@rowsncolumns/spreadsheet"
import { useSpreadsheet, InsertLinkDialog, InsertLinkEditor } from "@rowsncolumns/spreadsheet-state"

const Spreadsheet = () => {
  const {
    activeSheetId,
    activeCell,
    selections,
    onInsertLink,
    onRequestInsertLink
  } = useSpreadsheetState({})

  return (
    <>
      <button onClick={() => onRequestInsertLink()}>Insert link</button>
      <InsertLinkDialog>
        <InsertLinkEditor
          sheetId={activeSheetId}
          activeCell={activeCell}
          selections={selections}
          onInsertLink={onInsertLink}
        />
      </InsertLinkDialog>
    </>
  )
}

const App = () => (
  <SpreadsheetProvider>
    <Spreadsheet />
  </SpreadsheetProvider>
)
```

{% endcode %}

## Removing Links

Use the `onRemoveLink` callback to remove hyperlinks from cells:

```tsx
import { CanvasGrid, SpreadsheetProvider } from "@rowsncolumns/spreadsheet"
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state"

const Spreadsheet = () => {
  const {
    activeSheetId,
    activeCell,
    selections,
    onInsertLink,
    onRemoveLink,
    onRequestInsertLink
  } = useSpreadsheetState({})

  const handleRemoveLink = () => {
    onRemoveLink?.(activeSheetId, activeCell, selections);
  };

  return (
    <>
      <button onClick={() => onRequestInsertLink()}>Insert Link</button>
      <button onClick={handleRemoveLink}>Remove Link</button>

      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        onRemoveLink={onRemoveLink}
        // ... other props
      />
    </>
  )
}
```

## Complete Example with Context Menu

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
  InsertLinkDialog,
  InsertLinkEditor,
} from "@rowsncolumns/spreadsheet-state";

function SpreadsheetWithLinks() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet 1" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
    onInsertLink,
    onRemoveLink,
    onRequestInsertLink,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  // Check if active cell has a link
  const cellData = getCellData(
    activeSheetId,
    activeCell.rowIndex,
    activeCell.columnIndex
  );
  const hasLink = !!cellData?.hyperlink;

  return (
    <SpreadsheetProvider>
      <div className="flex flex-col h-screen">
        <div className="p-2 border-b">
          <button
            onClick={() => onRequestInsertLink()}
            className="px-3 py-1 bg-blue-500 text-white rounded mr-2"
          >
            Insert Link
          </button>
          <button
            onClick={() => onRemoveLink?.(activeSheetId, activeCell, selections)}
            disabled={!hasLink}
            className="px-3 py-1 bg-red-500 text-white rounded disabled:opacity-50"
          >
            Remove Link
          </button>
        </div>

        <div className="flex-1">
          <CanvasGrid
            sheetId={activeSheetId}
            activeCell={activeCell}
            selections={selections}
            getCellData={getCellData}
            onChangeActiveCell={onChangeActiveCell}
            onChangeSelections={onChangeSelections}
            onRemoveLink={onRemoveLink}
          />
        </div>

        <InsertLinkDialog>
          <InsertLinkEditor
            sheetId={activeSheetId}
            activeCell={activeCell}
            selections={selections}
            onInsertLink={onInsertLink}
          />
        </InsertLinkDialog>
      </div>
    </SpreadsheetProvider>
  );
}

export default SpreadsheetWithLinks;
```

## Function Signatures

### onInsertLink

```typescript
type OnInsertLink = (
  sheetId: number,
  activeCell: CellInterface,
  selections: SelectionArea<SelectionAttributes>[],
  url: string,
  text?: string
) => void;
```

### onRemoveLink

```typescript
type OnRemoveLink = (
  sheetId: number,
  activeCell: CellInterface,
  selections: SelectionArea<SelectionAttributes>[]
) => void;
```

## Use Cases

### Adding Links to External Resources

```tsx
// Link to documentation
onInsertLink(sheetId, activeCell, selections, "https://docs.example.com", "Docs");

// Link to related file
onInsertLink(sheetId, activeCell, selections, "/files/report.pdf", "View Report");
```

### Batch Link Removal

Remove links from multiple cells:

```tsx
const removeBatchLinks = () => {
  // Remove links from all selected cells
  selections.forEach((selection) => {
    onRemoveLink?.(activeSheetId, activeCell, [selection]);
  });
};
```

## Best Practices

1. **Validate URLs**: Ensure URLs are properly formatted before insertion
2. **Provide Feedback**: Show visual indicators for cells with links
3. **Handle Errors**: Gracefully handle invalid or broken links
4. **Keyboard Shortcuts**: Support Ctrl/Cmd+K for inserting links
5. **Context Menu**: Add link operations to the cell context menu


# Insert Image Editor

Adds a image editor

<figure><img src="/files/TMGCin8Mn8E7yF9lHTFl" alt=""><figcaption><p>Insert Image editor</p></figcaption></figure>

{% code overflow="wrap" %}

```tsx
import { CanvasGrid, SpreadsheetProvider } from "@rowsncolumns/spreadsheet"
import { useSpreadsheet, InsertImageDialog, InsertImageEditor } from "@rowsncolumns/spreadsheet-state"

const Spreadsheet = () => {
  const {
    activeSheetId,
    activeCell,
    selections,
    onInsertImage,
    onRequestInsertImage
  } = useSpreadsheetState({})
  
  return (
    <>
      <button onClick={() => onRequestInsertImage()}>Insert image</button>
      <InsertImageDialog>
        <InsertImageEditor
          sheetId={activeSheetId}
          activeCell={activeCell}
          selections={selections}
          onInsertImage={onInsertImage}
        />
      </InsertImageDialog>
    </>
  )
}

const App = () => (
  <SpreadsheetProvider>
    <Spreadsheet />
  </SpreadsheetProvider>
)
```

{% endcode %}


# Floating Cell Editor

Floating Cell Editor component can be used to create Mobile editors that resides out of the Spreadsheet container.

<figure><img src="/files/o2cLT13UhElrAoeioK66" alt=""><figcaption><p>Mobile or Floating Cell Editor</p></figcaption></figure>

To insert a mobile or floating cell editor

{% code overflow="wrap" %}

```tsx
import { FloatingCellEditor, defaultSpreadsheetTheme } from "@rowsncolumns/spreadsheet"
import { functionDescriptions } from "@rowsncolumns/functions";

const App = () => {
  const [theme, onChangeTheme] = useState<SpreadsheetTheme>(
      defaultSpreadsheetTheme
    );
  const {
    activeSheetId,
    activeCell,
    selections,
    getUserEnteredValue,
    getEffectiveFormat,
    onChange,
    onChangeFormatting,
    onInsertRow,
    onInsertColumn 
  } = useSpreadsheetState({ ... })

  // Format of the current cell
  const currentCellFormat = useMemo(
    () =>
      getEffectiveFormat(
        activeSheetId,
        activeCell.rowIndex,
        activeCell.columnIndex
      ),
    [activeSheetId, activeCell, getEffectiveFormat]
  );

  return (
    <FloatingCellEditor
      initialValue={getUserEnteredValue(
        activeSheetId,
        activeCell.rowIndex,
        activeCell.columnIndex
      )}
      theme={theme}
      sheetId={activeSheetId}
      activeCell={activeCell}
      selections={selections}
      onChange={onChange}
      cellFormat={currentCellFormat}
      onChangeFormatting={onChangeFormatting}
      onInsertRow={onInsertRow}
      onInsertColumn={onInsertColumn}
      functionDescriptions={functionDescriptions}
    />
  )
}
```

{% endcode %}


# Grid Footer

Footer component for adding rows dynamically at the bottom of the spreadsheet

The `GridFooter` component provides a customizable footer area at the bottom of the spreadsheet grid, allowing users to add additional rows dynamically.

## Overview

GridFooter displays at the bottom of the spreadsheet canvas and typically includes controls for adding new rows to the end of the sheet. It's useful for applications where users frequently add data at the bottom of their spreadsheet.

## Basic Usage

```tsx
import { SpreadsheetProvider, CanvasGrid, GridFooter } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function MySpreadsheet() {
  const {
    activeSheetId,
    onRequestAddRows,
    // ... other hook values
  } = useSpreadsheetState({
    // ... configuration
  });

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={activeSheetId}
        footerHeight={80}
        footerComponent={
          <GridFooter
            sheetId={activeSheetId}
            onRequestAddRows={onRequestAddRows}
          />
        }
        // ... other props
      />
    </SpreadsheetProvider>
  );
}
```

## Props

### GridFooter Props

| Prop               | Type                                                | Description                             |
| ------------------ | --------------------------------------------------- | --------------------------------------- |
| `sheetId`          | `number`                                            | The ID of the active sheet              |
| `onRequestAddRows` | `(sheetId: number, additionalRows: number) => void` | Callback when user requests to add rows |

### CanvasGrid Props (for footer)

| Prop              | Type              | Description                         |
| ----------------- | ----------------- | ----------------------------------- |
| `footerHeight`    | `number`          | Height of the footer area in pixels |
| `footerComponent` | `React.ReactNode` | The footer component to render      |

## Features

### Add Rows Functionality

The GridFooter includes:

* An input field for specifying the number of rows to add
* An "Add" button to execute the action
* Automatic scrolling to the bottom after adding rows

```tsx
const { onRequestAddRows } = useSpreadsheetState({
  sheets,
  sheetData,
  onChangeSheets: setSheets,
  onChangeSheetData: setSheetData,
});

// GridFooter will call this when user clicks "Add"
// It automatically scrolls to the bottom after adding rows
<GridFooter
  sheetId={activeSheetId}
  onRequestAddRows={(sheetId, numRows) => {
    console.log(`Adding ${numRows} rows to sheet ${sheetId}`);
    onRequestAddRows?.(sheetId, numRows);
  }}
/>
```

### Implementing onRequestAddRows

The `onRequestAddRows` callback is typically provided by `useSpreadsheetState`, but you can also implement custom logic:

```tsx
import { useState } from "react";
import type { Sheet } from "@rowsncolumns/spreadsheet";

const [sheets, setSheets] = useState<Sheet[]>([
  { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet 1" }
]);

const handleAddRows = (sheetId: number, additionalRows: number) => {
  setSheets(prev => prev.map(sheet => {
    if (sheet.sheetId === sheetId) {
      return {
        ...sheet,
        rowCount: sheet.rowCount + additionalRows,
      };
    }
    return sheet;
  }));
};

<GridFooter
  sheetId={activeSheetId}
  onRequestAddRows={handleAddRows}
/>
```

## Custom Footer Component

You can create your own custom footer component instead of using the default GridFooter:

```tsx
const CustomFooter = ({ sheetId }: { sheetId: number }) => {
  return (
    <div className="flex items-center justify-between p-4 bg-gray-100">
      <span>Sheet {sheetId}</span>
      <button onClick={() => console.log("Custom action")}>
        Custom Action
      </button>
    </div>
  );
};

<CanvasGrid
  footerHeight={60}
  footerComponent={<CustomFooter sheetId={activeSheetId} />}
/>
```

## Styling

The GridFooter component uses Tailwind CSS classes and can be customized through className props:

```tsx
// The default GridFooter styling includes:
// - Responsive padding
// - Border styling
// - Background color from theme
// - Proper alignment with row headers
```

## Layout Considerations

### Footer Height

The `footerHeight` prop determines the height of the footer area:

```tsx
<CanvasGrid
  footerHeight={80}  // 80 pixels for footer
  footerComponent={<GridFooter {...props} />}
/>
```

### Row Header Alignment

GridFooter automatically aligns with the row header column, ensuring consistent layout:

```tsx
// Internal padding matches ROW_HEADER_WIDTH constant
// Scrollbar space is automatically accounted for
```

## Complete Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  GridFooter,
  type Sheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  type SheetData,
} from "@rowsncolumns/spreadsheet-state";

function SpreadsheetWithFooter() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet 1" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
    onRequestAddRows,
    // ... other state values
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
        onChangeActiveCell={onChangeActiveCell}
        onChangeSelections={onChangeSelections}
        footerHeight={80}
        footerComponent={
          <GridFooter
            sheetId={activeSheetId}
            onRequestAddRows={onRequestAddRows}
          />
        }
      />
    </SpreadsheetProvider>
  );
}
```

## Best Practices

1. **Set appropriate footer height**: The default 80px works well for most cases, but adjust based on your content
2. **Validate row additions**: Consider limits on total row count to prevent performance issues
3. **Provide feedback**: Show loading states or success messages when adding rows
4. **Handle errors gracefully**: Validate the number input and handle edge cases

## Use Cases

* **Data entry applications**: Allow users to easily add more rows as they input data
* **Dynamic spreadsheets**: Support growing datasets without manual sheet configuration
* **Form-like interfaces**: Provide a way to add new records at the bottom of a list
* **Log viewers**: Allow users to append new log entries

## Performance

GridFooter is lightweight and doesn't impact spreadsheet performance. The component only re-renders when its props change, and adding rows is handled efficiently by the spreadsheet state management.


# API

Spreadsheet API in a nutshell


# Cell Data

Defines the data model for a cell

`CellData` is the per-cell record stored in the array/record that builds sheet data. It bundles everything we need to round-trip a cell: the user-entered value, the calculated value, formatting, hyperlinks, validation, comments, protection flags, pivot/expand state, plus a handful of fields the calc engine uses internally.

Most fields exist in both a **long form** (`userEnteredValue`, `effectiveValue`, `userEnteredFormat`, `formattedValue`) and a **short form** (`ue`, `ev`, `uf`, `fv`). The short form is preferred for new writes — it keeps serialized payloads compact and is what the toolkit emits on save. The long forms still load for backwards compatibility.

The same naming convention applies one level down inside `ExtendedValue` — use `nv` / `sv` / `bv` / `fv` / `ev` instead of `numberValue` / `stringValue` / `boolValue` / `formulaValue` / `errorValue`.

```typescript
export type CellData<
  T extends StructuredResult = StructuredResult,
  M extends Mention = Mention,
> = {
  /**
   * The value the user entered in the cell. e.g, 1234, "Hello", or
   * "=NOW()". Dates, times, and datetimes are represented as doubles
   * in serial format.
   */
  ue?: ExtendedValue;
  /**
   * The effective value of the cell. For formula cells this is the
   * calculated result; for literal cells it equals `ue`. Read-only —
   * the calculation engine writes this. Set only when hydrating from
   * a saved snapshot.
   */
  ev?: ExtendedValue & StructuredValue<T>;
  /**
   * The formatted display string (after the number format is
   * applied). Read-only.
   */
  fv?: string;
  /**
   * The user-entered format. New writes are merged onto any existing
   * format. Can be a full `CellFormat` or a `StyleReference` (a short
   * `{ sid }` ref into the workbook's cellXfs registry) — the registry
   * dedupes repeated formats.
   */
  uf?: CellFormat | StyleReference;
  /**
   * Shared-strings key for cells whose value is interned in the
   * workbook's shared-strings table. Always a string. Per-segment
   * rich-text formatting (bold / italic / color / `@mention` chips)
   * rides on the SharedStrings entry as `{ text, runs }`; see the
   * [Rich text formatting](../features/rich-text-formatting.md)
   * feature page.
   */
  ss?: string;
  /**
   * Hyperlink target. Either a plain URL string or a structured value
   * carrying the URL + display label + tooltip.
   */
  hyperlink?: string | HyperlinkValue;
  /**
   * Data-validation rule attached to the cell. Inline rule object or
   * an ID reference into the sheet-level validation registry.
   */
  dataValidation?: DataValidationRule | DataValidationRuleRecord["id"];
  /**
   * Plain-text note (Excel "comment" — single-author, not threaded).
   */
  note?: string;
  /**
   * Pointer into the threaded-comment store (Excel 2016+ replies and
   * resolved state).
   */
  commentThreadId?: string | number;
  /**
   * Citation reference (FILTER source attribution, etc.).
   */
  citationId?: Citation["id"];
  /**
   * Cell-level protection flags. Only take effect when sheet protection
   * is enabled. `locked` defaults to true in Excel, so this field is
   * usually set to `{ locked: false }` to opt a cell OUT of protection.
   */
  protection?: {
    locked?: boolean;
    hidden?: boolean;
  };
  /**
   * Image URL — for cells whose value is an embedded image.
   */
  imageUrl?: string;
  /**
   * Cell metadata marker — currently only "people" for mention chips.
   */
  metaType?: "people";
  /**
   * Array-formula spill range in A1 notation ("B5:B20"). Only set on
   * the anchor cell of an array formula; spilled cells in the range
   * carry no `af`. Round-trips to / from <f t="array" ref="..."/> in
   * xlsx.
   */
  af?: string;
  /**
   * Conditional-formatting results keyed by rule ID. Written by the
   * CF evaluator; consumers shouldn't set this directly.
   */
  conditionalFormattingResultById?: Record<string, CustomFormulaResult>;
  /**
   * Result of a formula-based data-validation rule.
   */
  dataValidationResult?: CustomFormulaResult;
  /**
   * Outline / pivot grouping markers used by the canvas to render
   * expand-collapse chevrons.
   */
  expandable?: boolean;
  expanded?: boolean;
  /**
   * Pivot grouping key tuple for the cell.
   */
  groupKeys?: string[];
  childrenCount?: number;
  /**
   * Pivot table this cell belongs to.
   */
  pivotId?: PivotTable["pivotId"];
  /**
   * Collaboration / sync metadata.
   */
  version?: number;
  updatedAt?: number;

  // -------------------------------------------------------------------
  // Long-form aliases — kept for backwards compatibility with older
  // saved payloads. Prefer the short forms (`ue` / `ev` / `uf` / `fv`)
  // for new writes.
  // -------------------------------------------------------------------
  /** @deprecated use `ue` */
  userEnteredValue?: ExtendedValue;
  /** @deprecated use `ev` */
  effectiveValue?: ExtendedValue & StructuredValue<T>;
  /** @deprecated use `fv` */
  formattedValue?: string;
  /** @deprecated use `uf` */
  userEnteredFormat?: CellFormat | StyleReference;
};
```

{% hint style="info" %}
CellData is the source of truth of a cell. Spreadsheet does not mutate cellData — cells are rendered based on the properties on the record.
{% endhint %}

## ev and ue

`ev` (effective value) is derived from the value the user entered. If the user enters `"1000"`, Spreadsheet detects the type and classifies it as `nv` (number).

If the user enters `=SUM(4,4)`, `ev` is set when the calculation engine returns the result — `nv: 8` in this case.

Internally `ev` is used for calculations and `ue` is used for cell editing.

## uf and the effective format

All user-modified format changes are stored on `uf` (user-entered format). The effective format used at render time is **not** persisted on `CellData` — the renderer derives it on the fly from `uf`, the cell's effective value, and (for formula cells) precedent formats via `useSheetProperties.getEffectiveFormat`.

The legacy `effectiveFormat` / `ef` fields are accepted when loading older saved data but **should not be written**. New writes set `uf` only.

## Using your own CellData object

CanvasGrid accepts a custom CellData type that extends the core type:

```typescript
import {
  SpreadsheetProvider,
  CanvasGrid,
  CellData,
} from "@rowsncolumns/spreadsheet";

export type MyCustomCellData = CellData & {
  highlight?: boolean;
};

const App = () => {
  return (
    <SpreadsheetProvider>
      <CanvasGrid<MyCustomCellData>
        rowCount={1000}
        columnCount={1000}
        sheetId={1}
      />
    </SpreadsheetProvider>
  );
};
```


# Sheets

Supports multiple sheets with cross-references and tables

Developers can define any `Sheet` type, as CanvasGrid is agnostic of sheets. The sheet model used by `useSpreadsheetState` hook is

<https://github.com/rowsncolumns/spreadsheet/blob/main/apps/spreadsheet/types.ts#L6>

```typescript
export type Sheet = {
  title: string;
  sheetId: number;
  tabColor?: string;
  hidden?: boolean;
  rowCount?: number;
  columnCount?: number;
  frozenRowCount?: number;
  frozenColumnCount?: number;
  hideGridlines?: boolean;
  merges?: GridRange[];
  basicFilter?: FilterView;
  rowMetadata?: (DimensionProperties | null)[];
  columnMetadata?: (DimensionProperties | null)[];
};
```

## Sheet components

The following Sheet components are packaged with Spreadsheet

* NewSheetButton
* SheetSwitcher
* SheetTabs
* SheetStatus
* TableEditor
* DeleteSheetConfirmation

## Using your own Sheet type

You can extend from the core `Sheet` type and add your custom attributes

```typescript
import { SpreadsheetProvider, CanvasGrid, Sheet } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state"

type MyCustomSheet = Sheet & {
  owner: string;
  module: string
}

const Spreadsheet = () => {
  const [ sheets, onChangeSheets ] = useState<MyCustomSheet[]>([])
  const { } = useSpreadsheetState({
    sheets,
    onChangeSheets
  })
  return (
    <CanvasGrid
      rowCount={1000}
      columnCount={1000}
      sheetId={1}
    />
  );
};

export const App = () => (
  <SpreadsheetProvider>
    <Spreadsheet />
  <SpreadsheetProvider />
)
```


# SpreadsheetProvider

Context Provider that isolates Spreadsheet internal state

Each Spreadsheet instance should be wrapped in a SpreadsheetProvider component so that each instance will have its own set of props and internal state.

## useSpreadSheet hook

Children of SpreadsheetProvider will have access to `useSpreadsheet` hook that exposes several internal functions a developer can access.

```tsx
import {
  SpreadsheetProvider,
  CanvasGrid,
  useSpreadsheet,
} from "@rowsncolumns/spreadsheet";
import { tokenize as tokenizer } from "@rowsncolumns/calculator";

const MySpreadsheet = () => {

  const {
    canEditCell,
    cancelEditor,
    focusSheet,
    getCellBounds,
    getCellDimensions,
    getNamedRanges,
    getNextFocusableCell,
    getRowHeight,
    getSelectionsFromFormula,
    getTableColumnNames,
    getTableNames,
    makeEditable,
    onEditorKeyDown,
    scrollToCell,
    setEditorValue,
    submitEditor,
    updateSelectionStartEndReference,
  } = useSpreadsheet();
  
  return (
    <CanvasGrid
      rowCount={1000}
      columnCount={1000}
      sheetId={1}
      tokenizer={tokenizer}
    />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```

## Multiple Spreadsheets

You can render multiple Spreadsheet UI's in a single page, each able to work independently or together. It is left to your imagination.

You can share `tables, charts, embeds` across multiple sheets or even sheets and sheetData.

Each Spreadsheet should be wrapped in `<SpreadsheetProvider />` to isolate internal state, such as `formulaMode`, cell editor states, cell selection states etc.

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet"

const SpreadsheetA = () => {
  return (
    <SpreadsheetProvider>
      <CanvasGrid />
    </SpreadsheetProvider>
  )
}
const SpreadsheetB = () => {
  return (
    <SpreadsheetProvider>
      <CanvasGrid />
    </SpreadsheetProvider>
  )
}

const App = () => {
  return (
    <>
      <SpreadsheetA />
      <SpreadsheetB />
    </>
  )
}
```


# useSpreadsheet

useSpreadsheet exposes some of the internal API via hooks

{% hint style="info" %}
Wrap your app in `SpreadsheetProvider` to use `useSpreadsheet` hook
{% endhint %}

```tsx
import { SpreadsheetProvider, CanvasGrid, useSpreadsheet } from "@rowsncolumns/spreadsheet";

const MySpreadsheet = () => {
  const {
    makeEditable,
    canEditCell,
    cancelEditor,
    commit,
    focusSheet,
    getCellBounds,
    getCellDimensions,
    getContentfulGridRangeAroundCell,
    getNamedRanges,
    getNextFocusableCell,
    getRowHeight,
    getSelectionsFromFormula,
    getTableColumnNames,
    getTableNames,
    onEditorKeyDown,
    scrollToCell,
    setEditorValue,
    submitEditor,
    updateSelectionStartEndReference,
    tokenizer,
  } = useSpreadsheet();
  return (
    <CanvasGrid />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```

| API                                | Description                                                                                                                                             |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `makeEditable`                     | Manually trigger a cell to be in edit mode                                                                                                              |
| `canEditCell`                      | Verify if a cell is editable, respects protected ranges                                                                                                 |
| `cancelEditor`                     | Cancels edit mode                                                                                                                                       |
| `commit`                           | Commit the in-progress edit (submit if dirty, cancel if clean). Returns `true` if there was an edit to flush. Useful from a consumer-side blur handler. |
| `focusSheet`                       | Trigger focus on the Spreadsheet                                                                                                                        |
| `getCellBounds`                    | Get relative position of a cell relative to container                                                                                                   |
| `getCellDimensions`                | Get dimension (x, y, width, height) of a cell                                                                                                           |
| `getContentfulGridRangeAroundCell` | Get range around a specific cells that has content                                                                                                      |
| `getNamedRanges`                   | Get all named ranges of a sheet                                                                                                                         |
| `getNextFocusableCell`             | Get next available cell that can be focused. Skips hidden cells, rows and columns                                                                       |
| `getRowHeight`                     | Returns row height of a rowIndex                                                                                                                        |
| `getSelectionsFromFormula`         | Get all selections from text                                                                                                                            |
| `getTableColumnNames`              | Helper function to retrieve table column names                                                                                                          |
| `getTableNames`                    | Helper function to retrieve table names                                                                                                                 |
| `onEditorKeyDown`                  | Callback when user enters value in editor                                                                                                               |
| `scrollToCell`                     | Scroll to a cell                                                                                                                                        |
| `setEditorValue`                   | Imperatively set value in the editor                                                                                                                    |
| `submitEditor`                     | Submit value of a editor                                                                                                                                |
| `updateSelectionStartEndReference` | Internal function to update selection references                                                                                                        |
| `tokenizer`                        | Tokenizer formulas to identify selections and structured references                                                                                     |
| `flash`                            | Flash a range of cells with a specified color and duration                                                                                              |
| `showCellPopover`                  | Show a popover at a specific cell location with custom content                                                                                          |
| `dispatchEvent`                    | Dispatch events to the spreadsheet grid                                                                                                                 |
| `redrawGrid`                       | Manually trigger a redraw of the grid                                                                                                                   |

## Examples

### Flash cells

Flash a range of cells to draw user attention:

```tsx
const { flash } = useSpreadsheet();

// Flash a range with yellow color for 1.5 seconds
flash?.(
  {
    startRowIndex: 2,
    endRowIndex: 3,
    startColumnIndex: 2,
    endColumnIndex: 3,
    sheetId: activeSheetId,
  },
  "#ffcc00",
  1500
);
```

### Show cell popover

Display custom content in a popover at a specific cell:

```tsx
const { showCellPopover } = useSpreadsheet();

showCellPopover?.(
  {
    rowIndex: 2,
    columnIndex: 3,
    sheetId: 1,
  },
  () => <div>Custom popover content</div>
);
```

### Redraw grid

Manually trigger a grid redraw (useful after loading custom fonts):

```tsx
const { redrawGrid } = useSpreadsheet();

loadWebFont(["Lobster"]).then(() => {
  redrawGrid?.();
});
```


# Hooks

Utility hooks for advanced spreadsheet functionality

The spreadsheet provides several utility hooks for common operations beyond the main `useSpreadsheetState` hook.

## useNavigateToSheetRange

Navigate to a specific cell or range in a sheet, optionally with a visual flash effect.

### Basic Usage

```tsx
import { useNavigateToSheetRange } from "@rowsncolumns/spreadsheet";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

function MySpreadsheet() {
  const navigateToSheetRange = useNavigateToSheetRange();

  const handleGoToRange = () => {
    navigateToSheetRange?.({
      sheetId: 2,
      startRowIndex: 10,
      endRowIndex: 15,
      startColumnIndex: 5,
      endColumnIndex: 8,
    });
  };

  return (
    <button onClick={handleGoToRange}>
      Go to Sheet 2, Range F11:I16
    </button>
  );
}
```

### Parameters

```typescript
navigateToSheetRange(
  sheetRange: SheetRange,     // The range to navigate to
  {
    enableFlash?: boolean,       // Show flash effect (default: true)
    flashColor?: Color,          // Flash color (default: "#ffcc00")
    flashDuration?: number       // Flash duration in ms (default: 1500)
  }
)
```

### Features

* **Sheet switching**: Automatically switches to the target sheet
* **Cell positioning**: Sets active cell to the top-left of the range
* **Scrolling**: Scrolls the range into view
* **Visual feedback**: Optional flash effect to highlight the range

### Example with Custom Flash

```tsx
const navigateToSheetRange = useNavigateToSheetRange();

// Navigate with red flash for 2 seconds
navigateToSheetRange?.(
  {
    sheetId: 1,
    startRowIndex: 5,
    endRowIndex: 10,
    startColumnIndex: 3,
    endColumnIndex: 6,
  },
  {
    enableFlash: true,      // Enable flash
    flashColor: "#ff0000", // Red color
    flashDuration: 2000       // 2 seconds
  }  
);
```

### Use Cases

* **Jump to errors**: Navigate to cells with validation errors
* **Go to formulas**: Jump to cells referenced in formulas
* **Search results**: Navigate to search result cells
* **Links**: Implement cell links to other sheet locations
* **Named ranges**: Jump to named range locations

## useLoadingIndicator

Display a loading indicator during async operations.

### Basic Usage

```tsx
import { useLoadingIndicator, LoadingIndicator } from "@rowsncolumns/spreadsheet";

function MySpreadsheet() {
  const [showLoader, hideLoader] = useLoadingIndicator();

  const handleAsyncOperation = async () => {
    showLoader();
    try {
      await someAsyncOperation();
    } finally {
      hideLoader();
    }
  };

  return (
    <>
      <button onClick={handleAsyncOperation}>
        Load Data
      </button>
      <CanvasGrid />
      <LoadingIndicator />
    </>
  );
}
```

### API

The hook returns a tuple:

```typescript
const [showLoader, hideLoader] = useLoadingIndicator();
```

* `showLoader()`: Shows the loading indicator
* `hideLoader()`: Hides the loading indicator

### Example with Multiple Operations

```tsx
function SpreadsheetWithAsyncOps() {
  const [showLoader, hideLoader] = useLoadingIndicator();

  const loadData = async () => {
    showLoader();
    try {
      const data = await fetchSpreadsheetData();
      setSheetData(data);
    } catch (error) {
      console.error("Failed to load data:", error);
    } finally {
      hideLoader();
    }
  };

  const saveData = async () => {
    showLoader();
    try {
      await saveSpreadsheetData(sheetData);
    } finally {
      hideLoader();
    }
  };

  return (
    <>
      <button onClick={loadData}>Load</button>
      <button onClick={saveData}>Save</button>
      <CanvasGrid />
      <LoadingIndicator />
    </>
  );
}
```

### Component

The `LoadingIndicator` component must be rendered in your app (typically at the root level):

```tsx
<SpreadsheetProvider>
  <Toolbar />
  <CanvasGrid />
  <LoadingIndicator />  {/* Required for the hook to work */}
</SpreadsheetProvider>
```

### Use Cases

* **Data loading**: Show loader while fetching data from server
* **File import**: Display during Excel/CSV import operations
* **Calculations**: Show during long-running calculations
* **Export operations**: Display while generating exports

## useSpreadsheetApi

Access the imperative API for programmatic control of the spreadsheet.

### Basic Usage

```tsx
import { useSpreadsheetApi } from "@rowsncolumns/spreadsheet";

function MyComponent() {
  const api = useSpreadsheetApi();

  const updateCell = () => {
    api?.getActiveSheet()
      ?.getRange({ rowIndex: 1, columnIndex: 1 })
      .setValue("Hello World")
      .setFormat("backgroundColor", "#ffcc00");
  };

  return <button onClick={updateCell}>Update A1</button>;
}
```

See [Imperative Spreadsheet API](/getting-started/imperative-spreadsheet-api) for complete documentation.

## Complete Example

```tsx
import React, { useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  LoadingIndicator,
  useLoadingIndicator,
  useNavigateToSheetRange,
  useSpreadsheetApi,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  type SheetData,
} from "@rowsncolumns/spreadsheet-state";

function SpreadsheetWithHooks() {
  const [sheets, setSheets] = useState([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Sheet 1" },
    { sheetId: 2, rowCount: 100, columnCount: 26, title: "Sheet 2" },
  ]);
  const [sheetData, setSheetData] = useState<SheetData>({});

  return (
    <SpreadsheetProvider>
      <SpreadsheetControls
        sheets={sheets}
        sheetData={sheetData}
        onChangeSheets={setSheets}
        onChangeSheetData={setSheetData}
      />
    </SpreadsheetProvider>
  );
}

function SpreadsheetControls({ sheets, sheetData, onChangeSheets, onChangeSheetData }) {
  const [showLoader, hideLoader] = useLoadingIndicator();
  const navigateToSheetRange = useNavigateToSheetRange();
  const api = useSpreadsheetApi();

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
    onChangeActiveSheet,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets,
    onChangeSheetData,
  });

  const findAndNavigate = async () => {
    showLoader();
    try {
      // Simulate search operation
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      // Navigate to found cell
      navigateToSheetRange?.({
        sheetId: 2,
        startRowIndex: 15,
        endRowIndex: 15,
        startColumnIndex: 5,
        endColumnIndex: 5,
      });
    } finally {
      hideLoader();
    }
  };

  const updateCellProgrammatically = () => {
    api?.getActiveSheet()
      ?.getRange({ rowIndex: 1, columnIndex: 1 })
      .setValue("Updated via API");
  };

  return (
    <>
      <div className="toolbar">
        <button onClick={findAndNavigate}>
          Find and Navigate
        </button>
        <button onClick={updateCellProgrammatically}>
          Update A1
        </button>
      </div>

      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
        onChangeActiveCell={onChangeActiveCell}
        onChangeSelections={onChangeSelections}
        onChangeActiveSheet={onChangeActiveSheet}
      />

      <LoadingIndicator />
    </>
  );
}

export default SpreadsheetWithHooks;
```

## Hook Dependencies

### useNavigateToSheetRange

Requires `SpreadsheetProvider` context:

```tsx
<SpreadsheetProvider>
  {/* Component using useNavigateToSheetRange */}
</SpreadsheetProvider>
```

### useLoadingIndicator

Requires `LoadingIndicator` component to be rendered:

```tsx
<SpreadsheetProvider>
  <YourComponents />
  <LoadingIndicator />  {/* Required */}
</SpreadsheetProvider>
```

### useSpreadsheetApi

Requires `SpreadsheetProvider` and active spreadsheet state:

```tsx
<SpreadsheetProvider>
  {/* Component using useSpreadsheetApi */}
</SpreadsheetProvider>
```

## Best Practices

### useNavigateToSheetRange

1. **User feedback**: Always provide visual feedback when navigating (use flash effect)
2. **Bounds checking**: Ensure the range exists before navigating
3. **Sheet existence**: Verify the target sheet exists

### useLoadingIndicator

1. **Always hide**: Use try-finally to ensure hideLoader is called
2. **Error handling**: Show errors to users if operations fail
3. **Timeout**: Consider adding timeouts for long operations
4. **Multiple operations**: Use separate show/hide pairs for different operations

### useSpreadsheetApi

1. **Null checking**: Always use optional chaining (`api?.`)
2. **State management**: Prefer declarative state over imperative API when possible
3. **Batch operations**: Use batch methods for multiple changes

## TypeScript Support

All hooks are fully typed:

```typescript
import type { SheetRange, Color } from "@rowsncolumns/spreadsheet";

const navigateToSheetRange = useNavigateToSheetRange();
// Type: (range: SheetRange, flash?: boolean, color?: Color, duration?: number) => void

const [showLoader, hideLoader] = useLoadingIndicator();
// Type: [() => void, () => void]

const api = useSpreadsheetApi();
// Type: SpreadsheetAPI | null
```

## Troubleshooting

### useNavigateToSheetRange not working

Ensure you're inside `SpreadsheetProvider`:

```tsx
<SpreadsheetProvider>
  <YourComponent />  {/* Hook works here */}
</SpreadsheetProvider>
```

### LoadingIndicator not showing

Verify the component is rendered:

```tsx
<LoadingIndicator />  {/* Must be present */}
```

### useSpreadsheetApi returns null

The API is only available after the spreadsheet is mounted:

```tsx
useEffect(() => {
  if (api) {
    // API is ready
  }
}, [api]);
```


# Modules

Spreadsheet 2 comes with various modules to help you compose the spreadsheet that you like.

## Calculator

Calculator contains tools to parse formula strings and evaluate them. We use a custom version of [fast-formula-parser](https://github.com/LesterLyu/fast-formula-parser).

```
yarn add @rowsncolumns/calculator
```

## Calculator Web worker

You can also run calculations in a web worker. Only the evaluation is done in a worker thread, while the dependency graph and dependency parser remains in the main UI thread

```
yarn add @rowsncolumns/calculator-webworker
```

## DAG - Directed acyclic graph

The dag package is used to store cell dependencies. You can use it to get dependents and precedents of a cell

```
yarn add @rowsncolumns/dag
```

## Functions

The functions package contains built-in functions supported by Spreadsheet. It also contains function descriptions, parameters etc.

```
yarn add @rowsncolumns/functions
```

## Grid

This is the main canvas grid that powers the spreadsheet

```
yarn add @rowsncolumns/grid
```

## Icons

Contains all icons used by Spreadsheet

```
yarn add @rowsncolumns/icons
```

## Toolkit

All export/import functions are available in the toolkit

```
yarn add @rowsncolumns/toolkit
```

## Spreadsheet State

Exposes hooks to manage spreadsheet state. Only required if you are not using a custom state management solution

```
yarn add @rowsncolumns/spreadsheet-state
```

## UI Components

The UI package contains all shared components used by Spreadsheet. CSS is powered by [Stitches](https://stitches.dev/). We do have plans to migrate to tailwind

```
yarn add @rowsncolumns/ui
```

## Utilities

Shared sheet utility functions

```
yarn add @rowsncolumns/utils
```

## Y Spreadsheet

```json
yarn add @rowsncolumns/y-spreadsheet
```

## Pivot

The pivot package provides advanced pivot table functionality powered by DuckDB for row/column grouping and aggregations.

```
yarn add @rowsncolumns/pivot
```


# SheetCell

Sheet Cell is a primitive to generate CellData object

### How to use SheetCell

{% code overflow="wrap" %}

```tsx
import { SheetCell } from "@rowsncolumns/spreadsheet-state"

const sheetCell = new SheetCell()

// Extend if necessary from existing cellData
const existingCellData = {}
sheetCell.assign(sheetId, { rowIndex, columnIndex }, existingCellData) 

// Set user entered value
sheetCell.setUserEnteredValue("$20")

// Get cell data
const cellData = sheetCell.getCellData()
```

{% endcode %}

CellData is the cell representation used in Spreadsheet 2. Short-form keys (`ue` / `ev` / `uf` / `fv`) are preferred for new writes — see [Cell Data](/configuration/api/cell-data) for the full type. The same short-form convention applies inside `ExtendedValue`: `nv` (number), `sv` (string), `bv` (bool), `fv` (formula), `ev` (error).

```json
{
    "ue": {
        "sv": "20"
    },
    "ev": {
        "nv": 20
    },
    "fv": "$20",
    "uf": {
        "numberFormat": {
            "type": "CURRENCY",
            "pattern": "\"$\"#,##0"
        }
    }
}
```

{% hint style="info" %}
`effectiveFormat` / `ef` are no longer written on `CellData`. The renderer derives the effective format on the fly from `uf`, the cell value, and (for formula cells) precedent formats. Older saved data with `effectiveFormat` still loads.
{% endhint %}


# Using Spreadsheet with NextJS

SSR is supported, but Server components, not yet

Add `use client` at the top of your file, so this component will be part of client bundle

More info here - <https://beta.nextjs.org/docs/rendering/server-and-client-components#convention>

```tsx
"use client";
import "@rowsncolumns/spreadsheet/dist/spreadsheet.min.css";
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";

export const Sheet = () => {
  return (
    <SpreadsheetProvider>
      <CanvasGrid />
    </SpreadsheetProvider>
  );
};
```

## Loading Canvas in Node

NextJS throws error while compiling canvas during SSR. If you see these errors,

```
Error: Module not found: Can't resolve 'canvas'
Did you mean './canvas'?
```

```
ModuleParseError: Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
(Source code omitted for this binary file)
```

the solution would be to install `canvas` and add it as an external module in nextJS

```
yarn add canvas --dev
```

```typescript
/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack (config) {
    config.externals.push('canvas')
    return config
  }
}

module.exports = nextConfig
```

### CSS Specificity when importing stylesheet

NextJS does not respect the order of CSS imports. So there could be style conflicts if you are using tailwindcss in a nextJS project.

The proposed solution is in tailwindcss v3 is

1. Install `postcss-import` and add it to your `postcss.config.js`

```javascript
module.exports = {
  plugins: {
    "postcss-import": {},
    tailwindcss: {},
    autoprefixer: {},
  },
};
```

2. Import spreadsheet style using the import statement

```
@import "tailwindcss/base";
@import "@rowsncolumns/spreadsheet/dist/spreadsheet.min.css";

@tailwind components;
@tailwind utilities;
```

Credit to <https://github.com/radix-ui/themes/issues/109#issuecomment-1747345743> for the fix

In tailwindcss v4

```
@import "tailwindcss";
@import "@rowsncolumns/spreadsheet/dist/spreadsheet.min.css" layer(components);;
```

### (Optional) Using tailwindcss preset to configure spreadsheet styles

If you are still having problems with conflicting css in nextjs, this is another way to solve it.

Install the rowsncolumns tailwind preset

```sh
yarn add @rowsncolumns/tailwindcss-preset --dev

// or 
npm install @rowsncolumns/tailwindcss-preset
```

Update tailwindcss content and add the preset

```javascript
// tailwind.config.js
content: [
   ...// your files
   "./node_modules/@rowsncolumns/*/dist/**/*.js"
],
presets: [
  require('@rowsncolumns/tailwindcss-preset')
]
```

Import spreadsheet css variables

Add a css file `spreadsheet.css` and add contents from <https://github.com/rowsncolumns/spreadsheet/blob/main/apps/spreadsheet/spreadsheet.css>

Import this css file in global.css

```
// global.css
@tailwind base;
@tailwind components;
@tailwind utilities;

@import "./spreadsheet.css"
```


# Keyboard shortcuts

Use keyboard shortcuts to navigate, format and use formulas.

### Common actions

| Description         | Mac shortcut                      | Windows shortcut                        |
| ------------------- | --------------------------------- | --------------------------------------- |
| Select column       | Ctrl + Space                      | Ctrl + Space                            |
| Select row          | Shift + Space                     | Shift + Space                           |
| Select all          | <p>⌘ + A<br>⌘ + Shift + Space</p> | <p>Ctrl + A<br>Ctrl + Shift + Space</p> |
| Undo                | ⌘ + Z                             | Ctrl + Z                                |
| Redo                | <p>⌘ + Y<br>⌘ + Shift + Z</p>     | <p>Ctrl + Y<br>Ctrl + Shift + Z</p>     |
| Find                | ⌘ + F                             | Ctrl + F                                |
| Fill range          | ⌘ + Enter                         | Ctrl + Enter                            |
| Fill down           | ⌘ + D                             | Ctrl + D                                |
| Fill right          | ⌘ + R                             | Ctrl + R                                |
| Print               | ⌘ + P                             | Ctrl + P                                |
| Copy                | ⌘ + C                             | Ctrl + C                                |
| Cut                 | ⌘ + X                             | Ctrl + X                                |
| Paste               | ⌘ + V                             | Ctrl + V                                |
| Paste values only   | ⌘ + Shift + V                     | Ctrl + Shift + V                        |
| Insert new sheet    | Shift + F11                       | Shift + F11                             |
| Clear selected cell | Backspace or Delete               | Backspace or Delete                     |

### Format Cells

| Description              | Mac shortcut           | Windows shortcut       |
| ------------------------ | ---------------------- | ---------------------- |
| Bold                     | ⌘ + b                  | Cltr + B               |
| Underline                | ⌘ + U                  | Cltr + U               |
| Italic                   | ⌘ + I                  | Cltr + I               |
| Strikethrough            | ⌘ + Shift + X          | Cltr + Shift + X       |
| Centre align             | ⌘ + Shift + E          | Ctrl + Shift + E       |
| Left align               | ⌘ + Shift + L          | Ctrl + Shift + L       |
| Right align              | ⌘ + Shift + R          | Ctrl + Shift + R       |
| Apply top border         | Alt + Shift + 1        | Option + Shift + 1     |
| Apply right border       | Alt + Shift + 2        | Option + Shift + 2     |
| Apply bottom border      | Alt + Shift + 3        | Option + Shift + 3     |
| Apply left border        | Alt + Shift + 4        | Option + Shift + 4     |
| Remove borders           | Alt + Shift + 6        | Option + Shift + 6     |
| Apply outer border       | Alt + Shift + 7        | Option + Shift + 7     |
| Insert time              | ⌘ + Shift + ;          | Ctrl + Shift + ;       |
| Insert date              | ⌘ + ;                  | Ctrl + ;               |
| Insert date and time     | ⌘ + Option + Shift + ; | Ctrl + Alt + Shift + ; |
| Format as decimal        | Ctrl + Shift + 1       | Ctrl + Shift + 1       |
| Format as time           | Ctrl + Shift + 2       | Ctrl + Shift + 2       |
| Format as date           | Ctrl + Shift + 3       | Ctrl + Shift + 3       |
| Format as currency       | Ctrl + Shift + 4       | Ctrl + Shift + 4       |
| Format as percentage     | Ctrl + Shift + 5       | Ctrl + Shift + 5       |
| Format as exponent (TBD) | Ctrl + Shift + 6       | Ctrl + Shift + 6       |
| Clear formatting         | ⌘ + \\                 | Ctrl + \\              |
| Show format dialog       | ⌘ + Option + 1         | Ctrl + Alt + 1\|       |

### Navigate spreadsheet

| Description                | Mac shortcut                                | Windows shortcut                            |
| -------------------------- | ------------------------------------------- | ------------------------------------------- |
| Move to beginning of row   | Fn + Left arrow                             | Home                                        |
| Move to beginning of sheet | ⌘ + Fn + Left arrow                         | Ctrl + Home                                 |
| Move to end of row         | Fn + Right arrow                            | End                                         |
| Move to end of sheet       | ⌘ + Fn + Right arrow                        | Ctrl + End                                  |
| Scroll to active cell      | ⌘ + Backspace                               | Ctrl + Backspace                            |
| Move to next sheet         | <p>Option + Down arrow<br>⌘ + Page Down</p> | <p>Alt + Down arrow<br>Ctrl + Page Down</p> |
| Move to previous sheet     | <p>Option + Up arrow<br>⌘ + Page Up</p>     | <p>Alt + Up arrow<br>Ctrl + Page Up</p>     |

### Edit notes

| Description      | Mac shortcut | Windows shortcut |
| ---------------- | ------------ | ---------------- |
| Insert/edit note | Shift + F2   | Shift + F2       |

### Open menu

| Description  | Mac shortcut | Windows shortcut |
| ------------ | ------------ | ---------------- |
| Context menu | Shift + F10  | Shift + F10      |

### Add or change rows and columns

| Description                 | Mac shortcut   | Windows shortcut |
| --------------------------- | -------------- | ---------------- |
| Insert rows above           |                |                  |
| Insert rows below           |                |                  |
| Insert columns to the left  |                |                  |
| Insert columns to the right |                |                  |
| Delete rows                 |                |                  |
| Delete columns              |                |                  |
| Hide row                    | ⌘ + Option + 9 | Ctrl + Alt + 9   |
| Hide column                 | ⌘ + Option + 0 | Ctrl + Alt + 0   |

### Use formulas

| Description       | Mac shortcut | Windows shortcut |
| ----------------- | ------------ | ---------------- |
| Show all formulae | Ctrl + \~    | Ctrl + \~        |


# Server-side Spreadsheet

Update spreadsheet state and run formula evaluation on the backend

Use `Spreadsheet` from `libs/spreadsheet-state/interface/spreadsheet-interface.ts` when you want to apply spreadsheet operations on the server (API routes, workers, job processors), then evaluate formulas before persisting or broadcasting changes.

## Import

Use the server entrypoint:

```ts
import { Spreadsheet } from "@rowsncolumns/spreadsheet-state/server";
```

## 1) Initialize state

```ts
import type { CellData, Sheet, TableView } from "@rowsncolumns/spreadsheet";
import type { SheetData } from "@rowsncolumns/spreadsheet-state";
import { Spreadsheet } from "@rowsncolumns/spreadsheet-state/server";

const spreadsheet = new Spreadsheet();

const sheets: Sheet[] = [
  { sheetId: 1, rowCount: 200, columnCount: 26, title: "Sheet1" },
];
const sheetData: SheetData<CellData> = { 1: [] };
const tables: TableView[] = [];

spreadsheet.sheets = sheets;
spreadsheet.sheetData = sheetData;
spreadsheet.tables = tables;
```

## 2) Apply updates

Use `changeBatch` for cell values/formulas (single or multi-range):

```ts
spreadsheet.changeBatch(
  1,
  {
    startRowIndex: 1,
    endRowIndex: 1,
    startColumnIndex: 1,
    endColumnIndex: 1,
  },
  [[10]],
);

spreadsheet.changeBatch(
  1,
  {
    startRowIndex: 1,
    endRowIndex: 1,
    startColumnIndex: 2,
    endColumnIndex: 2,
  },
  [["=A1*2"]],
);
```

You can also use structural APIs like `insertRow`, `insertColumn`, `deleteRow`, `deleteColumn`, `changeFormatting`, `updateTable`, etc. on the same `Spreadsheet` instance.

## 3) Run evaluation

After backend edits, explicitly flush calculations:

```ts
const results = await spreadsheet.calculatePending();
```

Useful variants:

* `await spreadsheet.calculatePending()`: Runs only queued operations and returns calculated result entries.
* `await spreadsheet.flushCalculations()`: Runs queued operations but does not return result entries.
* `await spreadsheet.calculateAll()`: Scans current `sheetData` formulas and evaluates them.

If you replace `sheetData` directly (outside `changeBatch`), call `calculateAll()` for a full formula pass, or call `rebuildGraph()` and enqueue explicit operations before `calculatePending()`.

## 4) Read updated values

`calculatePending`/`flushCalculations`/`calculateAll` apply results back into `spreadsheet.sheetData` via the internal calculation pipeline.

```ts
const cellB1 = spreadsheet.sheetData[1]?.[1]?.values?.[2];
```

## 5) Persist or broadcast patches

Every mutation pushes history patches that you can forward to persistence/collab layers.

```ts
const patchTuples = spreadsheet.getPatchTuples(); // for undo/redo style patch consumers
const fullStatePatches = await spreadsheet.generateStatePatches(); // full-state patch export
spreadsheet.clearPatches(); // optional reset after persisting/broadcasting
```

## Worker configuration (recommended)

For backend runtimes, provide a calculation worker explicitly:

```ts
import { Worker } from "node:worker_threads";
import { Spreadsheet } from "@rowsncolumns/spreadsheet-state/server";

const spreadsheet = new Spreadsheet({
  createCalculationWorker: () =>
    new Worker(require.resolve("@rowsncolumns/calculation-worker/worker")),
});
```


# Real time collaboration

Built for real-time editing and viewing of Spreadsheets

With a supported real-time back-end, you can have a full collaborative and real-time Spreadsheet. The UI is data agnostic, so both OT and CRDT data structures are supported.

SheetGrid exposes the following prop to highlight users in the canvas grid.

```tsx
const App = () => {
  const currentUserId = 1
  <SheetGrid
    users={
      [
        {
          userId: 1,
          title: 'Foo bar',
          // Active cell that will be highlighted
          activeCell: { rowIndex: 2, columnIndex: 3},
          // Which sheetID is currently in focus
          sheetId: 1
        },
        {
          userId: 2,
          title: 'Foo bar',
          activeCell: { rowIndex: 2, columnIndex: 3},
          sheetId: 1
        }
      ]
    }
    userId={currentUserId}
  />
}
```

## Immer and useSpreadsheetState

If you are using `useSpreadsheetState` hook to manage the state of Spreadsheet, Immer is the state library that is used to modify state.

Immer has really good API for JSON patches that is sent over the wire for real-time collaboration. JSON patches also helps in building a robust undo/redo functionality.

`onChangeHistory` callback is fired when user modifies state.

```tsx
import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";
import { tokenize as tokenizer } from "@rowsncolumns/calculator";
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const MySpreadsheet = () => {
  const {} = useSpreadsheetState({
    onChangeHistory(patches) {
      // Send this over the wire to Yjs, Pusher, Liveblocks etc
      console.log(patches);
    },
  });
  return (
    <CanvasGrid
      rowCount={1000}
      columnCount={1000}
      sheetId={1}
      tokenizer={tokenizer}
      users={[{ ... }]}
      userId={}
    />
  );
};

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
);
```


# Yjs (CRDT) Collaboration

Add real-time collaboration to Spreadsheet 2 using Yjs

There are two ways we can use yjs collaboration

1. Sending patches to yjs document and synchorizing these patches between users
2. Saving the entire data, like sheetData, sheets in yjs document. More performance and easy to restore an entire document

You can install Yjs hook as a separate module

{% tabs %}
{% tab title="yarn" %}

```sh
yarn add "@rowsncolumns/y-spreadsheet"
```

{% endtab %}

{% tab title="npm" %}

```sh
npm install "@rowsncolumns/y-spreadsheet"
```

{% endtab %}
{% endtabs %}

## Initializing Yjs

This hook uses `WebSocketProvider` from Yjs to initialize a connection to one of the demo servers `wss://demos.yjs.dev`

{% hint style="info" %}
The default provider can be easily switched to WebRTCProvider, but as of right now, WebRTC does not support `synced` events.
{% endhint %}

### Running yjs locally

```
HOST=localhost PORT=1234 npx y-websocket
```

```tsx
import {
  SheetData,
  useSpreadsheetState,
} from "@rowsncolumns/spreadsheet-state";

// Option 1
import { useYSpreadsheet } from "@rowsncolumns/yjs-spreadsheet"

// Option 2, Same API
import { useYSpreadsheetV2 } from "@rowsncolumns/yjs-spreadsheet"

import { SpreadsheetProvider, CanvasGrid } from '@rowsncolumns/spreadsheet'

const version = "v1"
const yDoc = new Y.Doc({ gc: true });
const yWebsocketProvider = new WebsocketProvider(
  "ws://localhost:1234",
  `y-spreadsheet-${version}`,
  yDoc,
  {
    connect: true,
  }
);

const MySpreadsheet = () => {
  const userId = 'foobar'
  const name = 'foobar'
  const [sheets, onChangeSheets] = useState<Sheet[]>([]);
  const [sheetData, onChangeSheetData] = useState<SheetData<CellData>>({});
  const [tables, onChangeTables] = useState<TableView[]>([]);
  
  const { activeCell, activeSheetId enqueueCalculation, ... } = useSpreadsheetState({
    ....,
    onChangeHistory(patches) {
      onBroadcastPatch(patches);
    },
  })
  
  // Add Yjs hook
  const { users, onBroadcastPatch } = useYSpreadsheet({
    provider: yWebsocketProvider,
    doc: yDoc,
    // Stable document scope for awareness/leader election
    awarenessScopeId: `y-spreadsheet-${version}`,
    onChangeSheetData,
    onChangeSheets,
    onChangeTables,
    enqueueCalculation,
    sheetId: activeSheetId,
    activeCell,

    // User info
    userId,
    title: name,
  });


  return (
    <CanvasGrid
      {...}
      users={users}
      userId={userId}
    >
  )

}

const App = () => (
  <SpreadsheetProvider>
    <MySpreadsheet />
  </SpreadsheetProvider>
)
```

## Import excel file and applying updates to yjs


# ShareDB (OT) Collaboration

Real-time collaboration using ShareDB

ShareDB is an OT-based real-time database that enables collaborative editing with automatic conflict resolution. The `@rowsncolumns/sharedb` package provides a React hook for integrating ShareDB with your spreadsheet.

## Installation

{% tabs %}
{% tab title="yarn" %}

```sh
yarn add "@rowsncolumns/sharedb sharedb reconnecting-websocket"
```

{% endtab %}

{% tab title="npm" %}

```sh
npm install "@rowsncolumns/sharedb sharedb reconnecting-websocket"
```

{% endtab %}
{% endtabs %}

## Quick Start

```tsx
import { useShareDBSpreadsheet } from "@rowsncolumns/sharedb";
import ShareDBClient from "sharedb/lib/client";
import ReconnectingWebSocket from "reconnecting-websocket";

// Create ShareDB connection
const socket = new ReconnectingWebSocket("ws://localhost:8080");
const connection = new ShareDBClient.Connection(socket);

function SpreadsheetEditor() {
  const [sheetData, setSheetData] = useState({});
  const [sheets, setSheets] = useState([]);
  const [tables, setTables] = useState([]);
  const [sheetId, setSheetId] = useState(1);
  const [activeCell, setActiveCell] = useState({ rowIndex: 1, columnIndex: 1 });

  const { onBroadcastPatch, users, synced, isLeader } = useShareDBSpreadsheet({
    connection,
    collection: "spreadsheets",
    documentId: "my-spreadsheet",
    userId: "user-123",
    title: "John Doe",
    sheetId,
    activeCell,
    initialSheets: [],
    onChangeSheetData: setSheetData,
    onChangeSheets: setSheets,
    onChangeTables: setTables,
    onChangeActiveSheet: setSheetId,
    calculateNow,
    enqueueGraphOperation: (op) => {
      // Handle dependency graph updates for formula recalculation
    },
  });

  // Pass onBroadcastPatch to useSpreadsheetState
  return <Spreadsheet sheetData={sheetData} sheets={sheets} users={users} />;
}
```

## Integration with useSpreadsheetState

Connect the ShareDB adapter to your spreadsheet state:

```tsx
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";
import { useShareDBSpreadsheet } from "@rowsncolumns/sharedb";

function CollaborativeSpreadsheet() {
  const {
    sheetData,
    sheets,
    tables,
    charts,
    embeds,
    namedRanges,
    protectedRanges,
    conditionalFormats,
    dataValidations,
    pivotTables,
    cellXfs,
    sharedStrings,
    setSheetData,
    setSheets,
    setTables,
    setCharts,
    setEmbeds,
    setNamedRanges,
    setProtectedRanges,
    setConditionalFormats,
    setDataValidations,
    setPivotTables,
    setCellXfs,
    setSharedStrings,
    enqueueGraphOperation,
    sheetId,
    activeCell,
    calculateNow,
  } = useSpreadsheetState({
    // Your spreadsheet state config
  });

  const { onBroadcastPatch, users, synced } = useShareDBSpreadsheet({
    connection,
    collection: "spreadsheets",
    documentId: "doc-id",
    userId: currentUser.id,
    title: currentUser.name,
    sheetId,
    activeCell,
    initialSheets: [],
    calculateNow,
    onChangeSheetData: setSheetData,
    onChangeSheets: setSheets,
    onChangeTables: setTables,
    onChangeCharts: setCharts,
    onChangeEmbeds: setEmbeds,
    onChangeNamedRanges: setNamedRanges,
    onChangeProtectedRanges: setProtectedRanges,
    onChangeConditionalFormats: setConditionalFormats,
    onChangeDataValidations: setDataValidations,
    onChangePivotTables: setPivotTables,
    onChangeCellXfs: setCellXfs,
    onChangeSharedStrings: setSharedStrings,
    enqueueGraphOperation,
  });

  // Pass onBroadcastPatch to your spreadsheet
  // ...
}
```

## Setting Up a ShareDB Server

### Basic Server

Create a file `server.js`:

```javascript
const http = require("http");
const express = require("express");
const ShareDB = require("sharedb");
const WebSocket = require("ws");
const WebSocketJSONStream = require("@teamwork/websocket-json-stream");

// Initialize ShareDB
const backend = new ShareDB();

// Create Express app and HTTP server
const app = express();
const server = http.createServer(app);

// Create WebSocket server
const wss = new WebSocket.Server({ server });

// Handle WebSocket connections
wss.on("connection", (ws) => {
  const stream = new WebSocketJSONStream(ws);
  backend.listen(stream);
});

// Start server
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
  console.log(`ShareDB server listening on port ${PORT}`);
});
```

Install dependencies:

```bash
npm install express sharedb ws @teamwork/websocket-json-stream
```

Run the server:

```bash
node server.js
```

### Server with MongoDB Persistence

For production environments, persist data to MongoDB:

```bash
npm install sharedb-mongo mongodb
```

```javascript
const ShareDBMongo = require("sharedb-mongo");

// Connect to MongoDB
const db = new ShareDBMongo("mongodb://localhost:27017/spreadsheets");

// Initialize ShareDB with MongoDB
const backend = new ShareDB({ db });
```

### Server with PostgreSQL Persistence

```bash
npm install sharedb-postgres pg
```

```javascript
const ShareDBPostgres = require("sharedb-postgres");

const db = new ShareDBPostgres({
  connectionString: "postgresql://user:password@localhost:5432/spreadsheets",
});

const backend = new ShareDB({ db });
```

## Document Structure

The ShareDB document stores all spreadsheet data:

```typescript
type ShareDBSpreadsheetDoc = {
  // Cell data: flat map with keys like "1!A1" -> { value, sId, r, c }
  sheetData: Record<string, CellDataV3>;

  // Sheet definitions
  sheets: Sheet[];

  // Tables, charts, embeds
  tables: TableView[];
  charts: EmbeddedChart[];
  embeds: EmbeddedObject[];

  // Named ranges, protected ranges
  namedRanges: NamedRange[];
  protectedRanges: ProtectedRange[];

  // Conditional formats, data validations
  conditionalFormats: ConditionalFormatRule[];
  dataValidations: DataValidationRuleRecord[];

  // Pivot tables
  pivotTables: PivotTable[];

  // Cell formats and shared strings
  cellXfs: Record<string, CellFormat>;
  sharedStrings: Record<string, string>;
};
```

### Cell Key Format

Cell keys follow the pattern `${sheetId}!${A1Address}`:

| Key         | Description           |
| ----------- | --------------------- |
| `"1!A1"`    | Cell A1 on sheet 1    |
| `"2!B5"`    | Cell B5 on sheet 2    |
| `"1!AA100"` | Cell AA100 on sheet 1 |

### CellDataV3 Structure

Each cell value is stored with its position metadata:

```typescript
type CellDataV3<T> = {
  value: T; // The cell data (text, formula, number, etc.)
  sId: number; // Sheet ID
  r: number; // Row index (0-based)
  c: number; // Column index (0-based)
};
```

## Presence Awareness

The hook automatically manages presence, allowing you to display other users' cursor positions:

```tsx
const { users } = useShareDBSpreadsheet({ ... });

// Render collaborators in your grid
<CanvasGrid
  users={users}
  userId={currentUserId}
/>
```

Each user in the `users` array includes:

```typescript
type Collaborator = {
  userId: string;
  title: string;
  sheetId: number;
  activeCell: { rowIndex: number; columnIndex: number };
};
```

## Leader Election

The adapter automatically elects a leader among connected clients. The leader is responsible for coordinating recalculation operations.

```tsx
const { isLeader } = useShareDBSpreadsheet({ ... });

// Leader-specific logic
if (isLeader) {
  // This client is responsible for coordinating recalcs
}
```

## Error Handling

Handle connection errors with the `onError` callback:

```tsx
useShareDBSpreadsheet({
  // ...
  onError: (err) => {
    console.error("ShareDB error:", err);
    toast.error("Connection lost. Reconnecting...");
  },
});
```

## Why ShareDB for Spreadsheets?

ShareDB's [Operational Transformation (OT)](https://en.wikipedia.org/wiki/Operational_transformation) approach is particularly well-suited for spreadsheet collaboration compared to CRDT-based solutions like Yjs.

### Comparison with Y.js

| Aspect              | ShareDB (OT)                            | Yjs (CRDT)                                   |
| ------------------- | --------------------------------------- | -------------------------------------------- |
| Conflict Resolution | OT - Server determines canonical order  | CRDT - Automatic merge, eventual consistency |
| Server Required     | Yes                                     | Optional (P2P possible)                      |
| Recalculation       | Leader election makes coordination easy | Requires additional coordination layer       |
| Undo/Redo           | Manual (via Immer patches)              | Built-in                                     |
| Offline Support     | Requires server connection              | Full offline-first support                   |
| Audit Trails        | Sequential operation log                | Distributed history                          |
| Data Persistence    | MongoDB, PostgreSQL, etc.               | LevelDB, IndexedDB, etc.                     |

### Why OT Works Well for Spreadsheets

1. **Cell-level granularity** - The V3 flat map structure (`"sheetId!A1"` keys) maps perfectly to ShareDB's json0 OT type. Each cell is an independent key-value pair, so conflicts are rare.
2. **Server-centric calculation** - Spreadsheets typically need a server anyway for formula calculation, persistence, and permissions. OT's server authority aligns naturally with this architecture.
3. **Simpler undo/redo** - OT's sequential operation log makes history management straightforward. The server maintains a clear order of operations.
4. **Predictable conflict resolution** - When two users edit the same cell simultaneously (rare in practice), the server determines the winner. No surprising merged states.
5. **Batched operations** - Large operations (paste 1000 cells, delete rows) can be batched into a single atomic `submitOp` call, maintaining consistency.

### When to Choose ShareDB

* Need a traditional client-server architecture
* Want to leverage existing MongoDB/PostgreSQL infrastructure
* Prefer predictable OT semantics for conflict resolution
* Server-side formula calculation is required
* Need clean audit trails and operation history

### When to Choose Y.js

* Need offline-first capabilities
* Want peer-to-peer collaboration without a server
* Need built-in undo/redo support
* Decentralized architecture is preferred

For most spreadsheet use cases with a server backend, ShareDB provides a cleaner, more predictable collaboration model.




---

[Next Page](/llms-full.txt/1)

