'use client'; import { useId, useState } from 'react'; import { FDI_LOWER_LEFT_TO_RIGHT, FDI_UPPER_LEFT_TO_RIGHT, getToothShapeKind } from '@/components/treatment/fdiToothMeta'; import { ToothGlyph } from '@/components/ui/treatment/ToothGlyph'; import { Dropdown } from '@/components/ui/common/Dropdown'; import { FDI_CHART_DESIGNS, type FdiChartDesignId } from '@/components/ui/treatment/fdiChartDesigns'; import type { FdiToothId } from '@/types/treatment'; const TOOTH_NUMBER_GAP = 'mt-1'; /** Six teeth each side of the midline (anteriors): slightly larger glyph + wrapper. */ const MIDLINE_SIX = new Set([ '13', '12', '11', '21', '22', '23', '43', '42', '41', '31', '32', '33', ]); function quadrantMirrored(fdi: FdiToothId): boolean { const q = fdi[0]; return q === '2' || q === '3'; } interface FdiToothChartProps { selected: ReadonlySet; onToggle: (fdi: FdiToothId) => void; disabled?: boolean; } export function FdiToothChart({ selected, onToggle, disabled }: FdiToothChartProps) { const uid = useId().replace(/:/g, ''); const archPeak = 10; const [chartDesign, setChartDesign] = useState('clinical'); const TOOTH_TWEAKS: Record = { '18': { glyph: 'w-[1.9rem] h-[4.5rem]', offset: 7, rotate: -11 }, '17': { glyph: 'w-[2rem] h-[4.75rem]', offset: 8, rotate: -8 }, '16': { glyph: 'w-[2.1rem] h-[4.95rem]', offset: 9, rotate: -6 }, '15': { glyph: 'w-[1.8rem] h-[4.8rem]', offset: 9, rotate: -4 }, '14': { glyph: 'w-[1.8rem] h-[4.85rem]', offset: 9, rotate: -2 }, '13': { glyph: 'w-[1.9rem] h-[5.28rem]', offset: 8, rotate: -1 }, '12': { glyph: 'w-[1.8rem] h-[5.02rem]', offset: 7, rotate: -1 }, '11': { glyph: 'w-[1.8rem] h-[5.08rem]', offset: 7, rotate: -0.5 }, '21': { glyph: 'w-[1.8rem] h-[5.08rem]', offset: 7, rotate: 0.5 }, '22': { glyph: 'w-[1.8rem] h-[5.02rem]', offset: 7, rotate: 1 }, '23': { glyph: 'w-[1.9rem] h-[5.28rem]', offset: 8, rotate: 1 }, '24': { glyph: 'w-[1.8rem] h-[4.85rem]', offset: 9, rotate: 2 }, '25': { glyph: 'w-[1.8rem] h-[4.8rem]', offset: 9, rotate: 4 }, '26': { glyph: 'w-[2.1rem] h-[4.95rem]', offset: 9, rotate: 6 }, '27': { glyph: 'w-[2rem] h-[4.75rem]', offset: 8, rotate: 8 }, '28': { glyph: 'w-[1.9rem] h-[4.5rem]', offset: 7, rotate: 11 }, '48': { glyph: 'w-[1.9rem] h-[4.55rem]', offset: -7, rotate: -11 }, '47': { glyph: 'w-[2rem] h-[4.8rem]', offset: -8, rotate: -8 }, '46': { glyph: 'w-[2.15rem] h-[5rem]', offset: -9, rotate: -6 }, '45': { glyph: 'w-[1.82rem] h-[4.85rem]', offset: -9, rotate: -4 }, '44': { glyph: 'w-[1.82rem] h-[4.9rem]', offset: -9, rotate: -2 }, '43': { glyph: 'w-[1.88rem] h-[5.34rem]', offset: -8, rotate: -1 }, '42': { glyph: 'w-[1.76rem] h-[5.04rem]', offset: -7, rotate: -1 }, '41': { glyph: 'w-[1.74rem] h-[4.98rem]', offset: -7, rotate: -0.5 }, '31': { glyph: 'w-[1.74rem] h-[4.98rem]', offset: -7, rotate: 0.5 }, '32': { glyph: 'w-[1.76rem] h-[5.04rem]', offset: -7, rotate: 1 }, '33': { glyph: 'w-[1.88rem] h-[5.34rem]', offset: -8, rotate: 1 }, '34': { glyph: 'w-[1.82rem] h-[4.9rem]', offset: -9, rotate: 2 }, '35': { glyph: 'w-[1.82rem] h-[4.85rem]', offset: -9, rotate: 4 }, '36': { glyph: 'w-[2.15rem] h-[5rem]', offset: -9, rotate: 6 }, '37': { glyph: 'w-[2rem] h-[4.8rem]', offset: -8, rotate: 8 }, '38': { glyph: 'w-[1.9rem] h-[4.55rem]', offset: -7, rotate: 11 }, }; const toothSizeClass = (fdi: FdiToothId) => { const kind = getToothShapeKind(fdi); const mid = MIDLINE_SIX.has(fdi); switch (kind) { case 'incisor': return mid ? { wrapper: 'w-[2.1rem]', glyph: 'w-[1.68rem] h-[4.78rem]' } : { wrapper: 'w-[2rem]', glyph: 'w-[1.6rem] h-[4.6rem]' }; case 'canine': return mid ? { wrapper: 'w-[2.2rem]', glyph: 'w-[1.78rem] h-[5.05rem]' } : { wrapper: 'w-[2.1rem]', glyph: 'w-[1.7rem] h-[4.9rem]' }; case 'premolar': return mid ? { wrapper: 'w-[2.34rem]', glyph: 'w-[1.93rem] h-[5.02rem]' } : { wrapper: 'w-[2.25rem]', glyph: 'w-[1.85rem] h-[4.85rem]' }; case 'molar': return mid ? { wrapper: 'w-[2.52rem]', glyph: 'w-[2.12rem] h-[5.1rem]' } : { wrapper: 'w-[2.45rem]', glyph: 'w-[2.05rem] h-[4.95rem]' }; default: return { wrapper: 'w-[2.25rem]', glyph: 'w-[1.85rem] h-[4.85rem]' }; } }; const archOffset = (index: number, count: number, upper?: boolean) => { const center = (count - 1) / 2; const dist = Math.abs(index - center) / center; const curve = (1 - Math.pow(dist, 1.45)) * archPeak; return upper ? curve : -curve; }; const archRotate = (index: number, count: number) => { const center = (count - 1) / 2; const normalized = (index - center) / center; return normalized * 6; }; const Row = ({ teeth, upper }: { teeth: FdiToothId[]; upper?: boolean }) => (
{teeth.map((fdi, i) => { const kind = getToothShapeKind(fdi); const gid = `${uid}-g-${fdi}-${i}`; const isSel = selected.has(fdi); const size = toothSizeClass(fdi); const tweak = TOOTH_TWEAKS[fdi]; const offsetY = tweak ? tweak.offset : archOffset(i, teeth.length, upper); const rotate = tweak ? tweak.rotate : archRotate(i, teeth.length); const alignItems = upper ? 'items-end' : 'items-start'; return (
); })}
); return (

FDI tooth chart

Tap teeth to multi-select (FDI). Selection applies to the active treatment record until you save.

setChartDesign(e.target.value as FdiChartDesignId)} > {FDI_CHART_DESIGNS.map((d) => ( ))}

Selected: {selected.size === 0 ? '—' : [...selected].sort().join(', ')}

Upper arch

{FDI_UPPER_LEFT_TO_RIGHT.map((fdi) => { const size = toothSizeClass(fdi); const isSel = selected.has(fdi); return ( {fdi} ); })}
{FDI_LOWER_LEFT_TO_RIGHT.map((fdi) => { const size = toothSizeClass(fdi); const isSel = selected.has(fdi); return ( {fdi} ); })}

Lower arch

); }