npm package: @salloomd/teeth-selector
Description: A headless React component for dental tooth selection with bridge support. Follows the [shadcn/ui](https://ui.shadcn.com/) code style and patterns.
Latest version: 0.0.5

# @salloomd/teeth-selector

A headless React component for dental tooth selection with bridge support. Follows the [shadcn/ui](https://ui.shadcn.com/) code style and patterns.

## Installation

```bash
bun add @salloomd/teeth-selector
# or
npm install @salloomd/teeth-selector
```

## Features

- 🎨 **Headless** - Bring your own styles
- 🦷 **Complete dental chart** - All 32 teeth with FDI numbering
- 🌉 **Bridge support** - Select dental bridges between adjacent teeth
- 📱 **Touch support** - Drag selection works on mobile
- 🎯 **Fully typed** - Written in TypeScript
- ⚡ **Render props** - Full control over rendering

## Usage

### Basic Example (with Tailwind CSS)

```tsx
import { TeethSelector, TeethChart } from "@salloomd/teeth-selector";

function MyDentalForm() {
  return (
    <TeethSelector
      onChange={(selectedTeeth, bridges) => {
        console.log("Selected:", selectedTeeth);
        console.log("Bridges:", bridges);
      }}
    >
      {({ selectedTeeth, clearAll, selectUpper, selectLower }) => (
        <div className="space-y-4">
          <div className="flex gap-2">
            <button onClick={selectUpper}>Select Upper</button>
            <button onClick={selectLower}>Select Lower</button>
            {selectedTeeth.length > 0 && (
              <button onClick={clearAll}>Clear All</button>
            )}
          </div>

          <TeethChart
            width={300}
            height={500}
            renderTooth={({ tooth, isSelected, isInDragRange }, defaultEl) => (
              <path
                key={tooth.id}
                id={`teeth-${tooth.id}`}
                d={tooth.d}
                className={`
                  cursor-pointer transition-colors
                  ${isSelected ? "fill-amber-400 stroke-gray-600" : "fill-white stroke-gray-400"}
                  ${isInDragRange ? "fill-blue-300" : ""}
                  hover:fill-amber-200
                `}
                onClick={defaultEl.props.onClick}
                onMouseDown={defaultEl.props.onMouseDown}
                onMouseEnter={defaultEl.props.onMouseEnter}
              />
            )}
            renderBridge={({ fromId, toId, exists, position }, defaultEl) => (
              <g key={`${fromId}-${toId}`}>
                {exists && (
                  <line
                    x1={position.x1}
                    y1={position.y1}
                    x2={position.x2}
                    y2={position.y2}
                    stroke="#ff6b6b"
                    strokeWidth={4}
                    strokeDasharray="5,3"
                  />
                )}
                <circle
                  cx={position.midX}
                  cy={position.midY}
                  r={8}
                  className={`
                    cursor-pointer transition-all
                    ${
                      exists
                        ? "fill-blue-500 stroke-white stroke-2"
                        : "fill-gray-200 stroke-gray-400 opacity-0 hover:opacity-100"
                    }
                  `}
                  onClick={defaultEl.props.children[1].props.onClick}
                />
              </g>
            )}
          />
        </div>
      )}
    </TeethSelector>
  );
}
```

### TeethPreview (Read-only)

```tsx
import { TeethPreview } from "@salloomd/teeth-selector";

function Preview() {
  // String format: individual teeth or bridged ranges
  // "11, 21, [31, 33]" means teeth 11, 21, and bridged 31-32-33
  return (
    <TeethPreview
      teeth="11, 21, [31, 33]"
      width={150}
      height={250}
      renderTooth={({ tooth, isSelected }, defaultEl) => (
        <path
          key={tooth.id}
          id={`teeth-preview-${tooth.id}`}
          d={tooth.d}
          className={
            isSelected
              ? "fill-amber-400 stroke-gray-600"
              : "fill-white stroke-gray-400"
          }
        />
      )}
    />
  );
}
```

### Controlled Mode

```tsx
import { useState } from "react";
import {
  TeethSelector,
  TeethChart,
  type Tooth,
} from "@salloomd/teeth-selector";

function ControlledExample() {
  const [selectedTeeth, setSelectedTeeth] = useState<number[]>([11, 21]);
  const [bridges, setBridges] = useState<number[][]>([[11, 21]]);

  return (
    <TeethSelector
      selectedTeeth={selectedTeeth}
      bridges={bridges}
      onSelectionChange={(teeth: Tooth[]) => {
        setSelectedTeeth(teeth.map((t) => t.id));
      }}
      onBridgeChange={setBridges}
    >
      {(props) => (
        <TeethChart
          renderTooth={({ tooth, isSelected }, defaultEl) => (
            <path
              key={tooth.id}
              id={`teeth-${tooth.id}`}
              d={tooth.d}
              style={{
                fill: isSelected ? "#fbbf24" : "#fff",
                stroke: isSelected ? "#4b5563" : "#9ca3af",
                cursor: "pointer",
              }}
              onClick={defaultEl.props.onClick}
              onMouseDown={defaultEl.props.onMouseDown}
              onMouseEnter={defaultEl.props.onMouseEnter}
            />
          )}
        />
      )}
    </TeethSelector>
  );
}
```

## API Reference

### TeethSelector

The root component that manages selection state.

| Prop                   | Type                                             | Description                   |
| ---------------------- | ------------------------------------------------ | ----------------------------- |
| `defaultSelectedTeeth` | `number[]`                                       | Initial selected teeth IDs    |
| `defaultBridges`       | `number[][]`                                     | Initial bridges as pairs      |
| `selectedTeeth`        | `number[]`                                       | Controlled selected teeth     |
| `bridges`              | `number[][]`                                     | Controlled bridges            |
| `onSelectionChange`    | `(teeth: Tooth[]) => void`                       | Called when selection changes |
| `onBridgeChange`       | `(bridges: number[][]) => void`                  | Called when bridges change    |
| `onChange`             | `(teeth: Tooth[], bridges: number[][]) => void`  | Unified change callback       |
| `children`             | `(props: TeethSelectorRenderProps) => ReactNode` | Render function               |

#### Render Props

```ts
interface TeethSelectorRenderProps {
  selectedTeeth: number[];
  bridges: number[][];
  toggleTooth: (toothId: number) => void;
  toggleBridge: (fromId: number, toId: number) => void;
  selectUpper: () => void;
  selectLower: () => void;
  selectRange: (teethIds: number[]) => void;
  clearAll: () => void;
  isSelected: (toothId: number) => boolean;
  hasBridge: (fromId: number, toId: number) => boolean;
  getSelectedTeethData: () => Tooth[];
}
```

### TeethChart

SVG component for rendering the interactive tooth chart. Must be used inside `TeethSelector`.

| Prop                  | Type       | Default | Description            |
| --------------------- | ---------- | ------- | ---------------------- |
| `width`               | `number`   | `300`   | SVG width              |
| `height`              | `number`   | `500`   | SVG height             |
| `className`           | `string`   | -       | Additional class names |
| `enableDragSelection` | `boolean`  | `true`  | Enable drag to select  |
| `renderTooth`         | `function` | -       | Custom tooth renderer  |
| `renderBridge`        | `function` | -       | Custom bridge renderer |

### TeethPreview

Read-only component for displaying a teeth selection string.

| Prop           | Type       | Default | Description                             |
| -------------- | ---------- | ------- | --------------------------------------- |
| `teeth`        | `string`   | -       | Selection string (e.g., "11, [21, 23]") |
| `width`        | `number`   | `150`   | SVG width                               |
| `height`       | `number`   | `250`   | SVG height                              |
| `className`    | `string`   | -       | Additional class names                  |
| `renderTooth`  | `function` | -       | Custom tooth renderer                   |
| `renderBridge` | `function` | -       | Custom bridge renderer                  |

### Utilities

```ts
import {
  teethData, // All 32 teeth with metadata
  adjacentTeethPairs, // Valid bridge pairs
  getTeethInRange, // Get teeth between two IDs
  parseTeethSelection, // Parse string to selection
  formatTeethSelection, // Format selection to string
} from "@salloomd/teeth-selector";
```

## Tooth Data Structure

```ts
interface Tooth {
  order: number; // Display order (0-31)
  id: number; // FDI tooth number (11-48)
  name: string; // Full name
  position: "upper" | "lower";
  side: "left" | "right";
  d: string; // SVG path data
}
```

## Dental Numbering (FDI)

The component uses FDI World Dental Federation notation:

- **Upper Right**: 18-11 (third molar to central incisor)
- **Upper Left**: 21-28 (central incisor to third molar)
- **Lower Left**: 38-31 (third molar to central incisor)
- **Lower Right**: 41-48 (central incisor to third molar)

## License

MIT