{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "molecule-viewer",
  "title": "Molecule viewer",
  "description": "Load a PDB or mmCIF structure and explore it as a cartoon, ball and stick or spacefill model, with residue selection and a sequence strip.",
  "dependencies": [
    "three@^0.186.0",
    "@base-ui/react@^1.8.0",
    "class-variance-authority@^0.7.1",
    "cn@^0.5.0"
  ],
  "devDependencies": [
    "@types/three@^0.186.0"
  ],
  "registryDependencies": [
    "alert",
    "button",
    "select",
    "tooltip"
  ],
  "files": [
    {
      "path": "registry/base-nova/molecule-viewer/molecule-viewer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport {\n  AmbientLight,\n  BufferAttribute,\n  BufferGeometry,\n  CatmullRomCurve3,\n  Color,\n  CylinderGeometry,\n  DirectionalLight,\n  Group,\n  HemisphereLight,\n  InstancedMesh,\n  Matrix4,\n  Mesh,\n  MeshStandardMaterial,\n  PerspectiveCamera,\n  Quaternion,\n  Raycaster,\n  Scene,\n  SphereGeometry,\n  Spherical,\n  Vector2,\n  Vector3,\n  WebGLRenderer,\n  type Intersection,\n  type Object3D,\n} from \"three\"\nimport { OrbitControls } from \"three/addons/controls/OrbitControls.js\"\n\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\n\nexport type MoleculeAtom = {\n  serial: number\n  /** Atom name, such as CA or O5'. */\n  name: string\n  /** Element symbol with conventional case, such as C, Fe or Se. */\n  element: string\n  x: number\n  y: number\n  z: number\n  /** Index into `MolecularStructure.residues`. */\n  residue: number\n}\n\nexport type ResidueKind = \"protein\" | \"nucleic\" | \"ligand\" | \"water\"\nexport type SecondaryStructure = \"helix\" | \"sheet\" | \"coil\"\n\nexport type MoleculeResidue = {\n  /** Chain, author sequence number and insertion code, such as `A:16` or `B:52A`. */\n  id: string\n  /** Residue or ligand code, such as CYS, DG or HEM. */\n  name: string\n  chain: string\n  seq: number\n  insertion: string\n  kind: ResidueKind\n  secondary: SecondaryStructure\n  /** Indices into `MolecularStructure.atoms`. */\n  atoms: number[]\n}\n\nexport type MolecularStructure = {\n  /** PDB ID when the file declares one. */\n  id?: string\n  title?: string\n  atoms: MoleculeAtom[]\n  residues: MoleculeResidue[]\n  chains: string[]\n  /** Covalent bonds as pairs of atom indices, inferred from distances. */\n  bonds: Uint32Array\n}\n\nexport type MoleculeRepresentation = \"cartoon\" | \"ball-and-stick\" | \"spacefill\"\nexport type MoleculeColorScheme =\n  \"secondary\" | \"chain\" | \"element\" | \"residue\" | \"rainbow\"\n\nexport type MoleculeViewerHandle = {\n  zoom: (factor: number) => void\n  rotate: (azimuth: number, polar?: number) => void\n  reset: () => void\n}\n\nexport type MoleculeViewerProps = {\n  /** URL of a PDB or mmCIF file. Ignored when `data` is given. */\n  src?: string\n  /** PDB or mmCIF text. */\n  data?: string\n  /** Detected from the text when omitted. */\n  format?: \"pdb\" | \"mmcif\"\n  representation?: MoleculeRepresentation\n  defaultRepresentation?: MoleculeRepresentation\n  onRepresentationChange?: (representation: MoleculeRepresentation) => void\n  colorScheme?: MoleculeColorScheme\n  defaultColorScheme?: MoleculeColorScheme\n  onColorSchemeChange?: (scheme: MoleculeColorScheme) => void\n  /** Residue ID, such as `A:16`. */\n  selectedId?: string | null\n  onSelectionChange?: (\n    id: string | null,\n    residue: MoleculeResidue | null\n  ) => void\n  /** Called once a structure is parsed. */\n  onLoad?: (structure: MolecularStructure) => void\n  /** Shows water molecules. Defaults to false. */\n  water?: boolean\n  /** Shows the selectable sequence below the stage. Defaults to true. */\n  sequence?: boolean\n  /** Shows the style and colour menus, zoom and reset buttons. Defaults to true. */\n  controls?: boolean\n  /** Links the entry on RCSB PDB when the file names its ID. Defaults to true. */\n  attribution?: boolean\n  label?: string\n  className?: string\n  ref?: React.Ref<MoleculeViewerHandle>\n}\n\nexport const moleculeRepresentations: Record<MoleculeRepresentation, string> = {\n  cartoon: \"Cartoon\",\n  \"ball-and-stick\": \"Ball and stick\",\n  spacefill: \"Spacefill\",\n}\n\nexport const moleculeColorSchemes: Record<MoleculeColorScheme, string> = {\n  secondary: \"Secondary structure\",\n  chain: \"Chain\",\n  element: \"Element\",\n  residue: \"Residue type\",\n  rainbow: \"Rainbow\",\n}\n\n// ---------------------------------------------------------------------------\n// Parsing\n\ntype RawAtom = Omit<MoleculeAtom, \"residue\"> & {\n  residueName: string\n  chain: string\n  seq: number\n  insertion: string\n  hetero: boolean\n  /** Alternate location; conformers after the first are dropped. */\n  altLoc: string\n}\n\ntype Range = {\n  kind: \"helix\" | \"sheet\"\n  chain: string\n  start: string\n  end: string\n}\n\nconst WATER = new Set([\"HOH\", \"WAT\", \"DOD\", \"H2O\", \"SOL\", \"TIP\", \"TIP3\"])\nconst AMINO_ACIDS: Record<string, string> = {\n  ALA: \"A\",\n  ARG: \"R\",\n  ASN: \"N\",\n  ASP: \"D\",\n  CYS: \"C\",\n  GLN: \"Q\",\n  GLU: \"E\",\n  GLY: \"G\",\n  HIS: \"H\",\n  ILE: \"I\",\n  LEU: \"L\",\n  LYS: \"K\",\n  MET: \"M\",\n  PHE: \"F\",\n  PRO: \"P\",\n  SER: \"S\",\n  THR: \"T\",\n  TRP: \"W\",\n  TYR: \"Y\",\n  VAL: \"V\",\n  SEC: \"U\",\n  PYL: \"O\",\n  MSE: \"M\",\n}\nconst NUCLEOTIDES: Record<string, string> = {\n  A: \"A\",\n  C: \"C\",\n  G: \"G\",\n  U: \"U\",\n  T: \"T\",\n  I: \"I\",\n  DA: \"A\",\n  DC: \"C\",\n  DG: \"G\",\n  DT: \"T\",\n  DU: \"U\",\n  DI: \"I\",\n}\n\nfunction elementCase(symbol: string) {\n  const letters = symbol.replace(/[^a-z]/gi, \"\")\n  return letters\n    ? letters[0]!.toUpperCase() + letters.slice(1, 2).toLowerCase()\n    : \"\"\n}\n\n// Columns 77-78 are often blank in older files; the atom name's leading\n// letters then give the element, two letters only for names starting in\n// column 13.\nfunction pdbElement(line: string, name: string) {\n  const declared = elementCase(line.slice(76, 78).trim())\n  if (declared) return declared\n  const field = line.slice(12, 16)\n  return elementCase(\n    field[0] !== \" \" && /[A-Z]/i.test(field[1] ?? \"\")\n      ? field.slice(0, 2)\n      : (name.replace(/^[0-9]+/, \"\")[0] ?? \"C\")\n  )\n}\n\nfunction parsePdb(text: string) {\n  const atoms: RawAtom[] = []\n  const ranges: Range[] = []\n  let id: string | undefined\n  let title = \"\"\n  for (const line of text.split(/\\r?\\n/)) {\n    const record = line.slice(0, 6).trim()\n    if (record === \"ENDMDL\") break\n    if (record === \"HEADER\") id = line.slice(62, 66).trim() || undefined\n    else if (record === \"TITLE\")\n      title = `${title} ${line.slice(10).trim()}`.trim()\n    else if (record === \"HELIX\")\n      ranges.push({\n        kind: \"helix\",\n        chain: line[19]?.trim() ?? \"\",\n        start: line.slice(21, 26).trim(),\n        end: line.slice(33, 38).trim(),\n      })\n    else if (record === \"SHEET\")\n      ranges.push({\n        kind: \"sheet\",\n        chain: line[21]?.trim() ?? \"\",\n        start: line.slice(22, 27).trim(),\n        end: line.slice(33, 38).trim(),\n      })\n    else if (record === \"ATOM\" || record === \"HETATM\") {\n      const name = line.slice(12, 16).trim()\n      atoms.push({\n        serial: Number(line.slice(6, 11)),\n        name,\n        element: pdbElement(line, name),\n        x: Number(line.slice(30, 38)),\n        y: Number(line.slice(38, 46)),\n        z: Number(line.slice(46, 54)),\n        residueName: line.slice(17, 20).trim(),\n        chain: line[21]?.trim() ?? \"\",\n        seq: Number(line.slice(22, 26)),\n        insertion: line[26]?.trim() ?? \"\",\n        hetero: record === \"HETATM\",\n        altLoc: line[16]?.trim() ?? \"\",\n      })\n    }\n  }\n  return { id, title: title || undefined, atoms, ranges }\n}\n\ntype CifTable = { names: string[]; values: string[] }\n\n// A reader for the CIF 1.1 syntax PDBx/mmCIF files use: data blocks, loops,\n// quoted values and semicolon text fields. Values keep their raw text; `?`\n// and `.` mean unknown and not applicable.\nfunction readCif(text: string) {\n  const tokens: string[] = []\n  const quoted: boolean[] = []\n  const lines = text.split(/\\r?\\n/)\n  for (let i = 0; i < lines.length; i++) {\n    const line = lines[i]!\n    if (line.startsWith(\";\")) {\n      const field = [line.slice(1)]\n      while (++i < lines.length && !lines[i]!.startsWith(\";\"))\n        field.push(lines[i]!)\n      tokens.push(field.join(\"\\n\").trim())\n      quoted.push(true)\n      continue\n    }\n    let at = 0\n    while (at < line.length) {\n      const char = line[at]!\n      if (char === \" \" || char === \"\\t\") at++\n      else if (char === \"#\") break\n      else if (char === \"'\" || char === '\"') {\n        let end = at + 1\n        // A quote only closes when whitespace or the line end follows it.\n        while (\n          end < line.length &&\n          !(line[end] === char && /\\s|^$/.test(line[end + 1] ?? \"\"))\n        )\n          end++\n        tokens.push(line.slice(at + 1, end))\n        quoted.push(true)\n        at = end + 1\n      } else {\n        let end = at\n        while (end < line.length && !/\\s/.test(line[end]!)) end++\n        tokens.push(line.slice(at, end))\n        quoted.push(false)\n        at = end\n      }\n    }\n  }\n  const tables = new Map<string, CifTable>()\n  let id: string | undefined\n  const table = (category: string) => {\n    let found = tables.get(category)\n    if (!found) tables.set(category, (found = { names: [], values: [] }))\n    return found\n  }\n  const reserved = (i: number) =>\n    !quoted[i] && /^(_|loop_$|data_|save_|global_$|stop_$)/i.test(tokens[i]!)\n  for (let i = 0; i < tokens.length;) {\n    const token = tokens[i]!\n    if (!quoted[i] && token.toLowerCase().startsWith(\"data_\")) {\n      // Only the first data block is read.\n      if (id !== undefined || tables.size) break\n      id = token.slice(5)\n      i++\n    } else if (!quoted[i] && token.toLowerCase() === \"loop_\") {\n      i++\n      const names: string[] = []\n      while (i < tokens.length && !quoted[i] && tokens[i]!.startsWith(\"_\"))\n        names.push(tokens[i++]!)\n      const category = names[0]?.split(\".\")[0] ?? \"\"\n      const loop = table(category)\n      loop.names = names.map((name) => name.split(\".\")[1] ?? name)\n      while (i < tokens.length && !reserved(i)) loop.values.push(tokens[i++]!)\n    } else if (!quoted[i] && token.startsWith(\"_\")) {\n      const [category = \"\", field = \"\"] = token.split(\".\")\n      const item = table(category)\n      item.names.push(field)\n      item.values.push(tokens[i + 1] ?? \"?\")\n      i += 2\n    } else i++\n  }\n  return { id, tables }\n}\n\nfunction cifColumns(table: CifTable | undefined) {\n  if (!table || !table.names.length) return { rows: 0, get: () => \"\" }\n  const width = table.names.length\n  const index = new Map(table.names.map((name, i) => [name, i]))\n  return {\n    rows: Math.floor(table.values.length / width),\n    // The first known name wins, so author fields fall back to label fields.\n    get(row: number, ...names: string[]) {\n      for (const name of names) {\n        const column = index.get(name)\n        if (column === undefined) continue\n        const value = table.values[row * width + column] ?? \"\"\n        if (value !== \"?\" && value !== \".\") return value\n      }\n      return \"\"\n    },\n  }\n}\n\nfunction parseMmcif(text: string) {\n  const { id, tables } = readCif(text)\n  const site = cifColumns(tables.get(\"_atom_site\"))\n  const atoms: RawAtom[] = []\n  let model: string | undefined\n  for (let row = 0; row < site.rows; row++) {\n    const number = site.get(row, \"pdbx_PDB_model_num\")\n    model ??= number\n    if (number !== model) continue\n    const name = site.get(row, \"auth_atom_id\", \"label_atom_id\")\n    atoms.push({\n      serial: Number(site.get(row, \"id\")),\n      name,\n      element: elementCase(site.get(row, \"type_symbol\") || name[0] || \"C\"),\n      x: Number(site.get(row, \"Cartn_x\")),\n      y: Number(site.get(row, \"Cartn_y\")),\n      z: Number(site.get(row, \"Cartn_z\")),\n      residueName: site.get(row, \"auth_comp_id\", \"label_comp_id\"),\n      chain: site.get(row, \"auth_asym_id\", \"label_asym_id\"),\n      seq: Number(site.get(row, \"auth_seq_id\", \"label_seq_id\")),\n      insertion: site.get(row, \"pdbx_PDB_ins_code\"),\n      hetero: site.get(row, \"group_PDB\") === \"HETATM\",\n      altLoc: site.get(row, \"label_alt_id\"),\n    })\n  }\n  const ranges: Range[] = []\n  const conf = cifColumns(tables.get(\"_struct_conf\"))\n  for (let row = 0; row < conf.rows; row++)\n    if (conf.get(row, \"conf_type_id\").toUpperCase().startsWith(\"HELX\"))\n      ranges.push({\n        kind: \"helix\",\n        chain: conf.get(row, \"beg_auth_asym_id\", \"beg_label_asym_id\"),\n        start:\n          conf.get(row, \"beg_auth_seq_id\", \"beg_label_seq_id\") +\n          conf.get(row, \"pdbx_beg_PDB_ins_code\"),\n        end:\n          conf.get(row, \"end_auth_seq_id\", \"end_label_seq_id\") +\n          conf.get(row, \"pdbx_end_PDB_ins_code\"),\n      })\n  const sheet = cifColumns(tables.get(\"_struct_sheet_range\"))\n  for (let row = 0; row < sheet.rows; row++)\n    ranges.push({\n      kind: \"sheet\",\n      chain: sheet.get(row, \"beg_auth_asym_id\", \"beg_label_asym_id\"),\n      start:\n        sheet.get(row, \"beg_auth_seq_id\", \"beg_label_seq_id\") +\n        sheet.get(row, \"pdbx_beg_PDB_ins_code\"),\n      end:\n        sheet.get(row, \"end_auth_seq_id\", \"end_label_seq_id\") +\n        sheet.get(row, \"pdbx_end_PDB_ins_code\"),\n    })\n  const title = cifColumns(tables.get(\"_struct\")).get(0, \"title\") || undefined\n  const entry = cifColumns(tables.get(\"_entry\")).get(0, \"id\") || id\n  return { id: entry || undefined, title, atoms, ranges }\n}\n\n// Covalent radii in ångströms (Cordero et al. 2008), for bond inference.\nconst COVALENT: Record<string, number> = {\n  H: 0.31,\n  C: 0.76,\n  N: 0.71,\n  O: 0.66,\n  F: 0.57,\n  P: 1.07,\n  S: 1.05,\n  Cl: 1.02,\n  Se: 1.2,\n  Br: 1.2,\n  I: 1.39,\n  B: 0.84,\n  Si: 1.11,\n}\n// Metal ions coordinate rather than bond, so they are drawn on their own.\nconst METALS = new Set(\n  \"Li Na K Rb Cs Be Mg Ca Sr Ba Mn Fe Co Ni Cu Zn Cd Hg Al Ga Pt Au Ag Mo W V Cr Ti Pb\".split(\n    \" \"\n  )\n)\n\nfunction inferBonds(atoms: MoleculeAtom[], residues: MoleculeResidue[]) {\n  const cell = 2.6\n  const grid = new Map<string, number[]>()\n  const key = (x: number, y: number, z: number) =>\n    `${Math.floor(x / cell)},${Math.floor(y / cell)},${Math.floor(z / cell)}`\n  atoms.forEach((atom, index) => {\n    if (METALS.has(atom.element)) return\n    const k = key(atom.x, atom.y, atom.z)\n    const bucket = grid.get(k)\n    if (bucket) bucket.push(index)\n    else grid.set(k, [index])\n  })\n  const bonds: number[] = []\n  atoms.forEach((a, i) => {\n    if (METALS.has(a.element)) return\n    const ra = COVALENT[a.element] ?? 0.8\n    const water = residues[a.residue]!.kind === \"water\"\n    const cx = Math.floor(a.x / cell)\n    const cy = Math.floor(a.y / cell)\n    const cz = Math.floor(a.z / cell)\n    for (let dx = -1; dx <= 1; dx++)\n      for (let dy = -1; dy <= 1; dy++)\n        for (let dz = -1; dz <= 1; dz++) {\n          const bucket = grid.get(`${cx + dx},${cy + dy},${cz + dz}`)\n          if (!bucket) continue\n          for (const j of bucket) {\n            if (j <= i) continue\n            const b = atoms[j]!\n            if (a.element === \"H\" && b.element === \"H\") continue\n            if (\n              (water || residues[b.residue]!.kind === \"water\") &&\n              a.residue !== b.residue\n            )\n              continue\n            const limit = ra + (COVALENT[b.element] ?? 0.8) + 0.4\n            const d2 = (a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2\n            if (d2 > 0.16 && d2 < limit * limit) bonds.push(i, j)\n          }\n        }\n  })\n  return Uint32Array.from(bonds)\n}\n\nfunction residueKind(name: string, atoms: RawAtom[]): ResidueKind {\n  if (WATER.has(name)) return \"water\"\n  const names = new Set(atoms.map((atom) => atom.name))\n  if (\n    names.has(\"CA\") &&\n    (name in AMINO_ACIDS || (names.has(\"N\") && names.has(\"C\")))\n  )\n    return \"protein\"\n  if (name in NUCLEOTIDES && (names.has(\"P\") || names.has(\"C4'\")))\n    return \"nucleic\"\n  return \"ligand\"\n}\n\n/** Parses PDB or PDBx/mmCIF text; only the first model and alternate location are kept. */\nexport function parseStructure(\n  text: string,\n  format?: \"pdb\" | \"mmcif\"\n): MolecularStructure {\n  const cif =\n    format === \"mmcif\" ||\n    (!format && /^\\s*(#.*\\n\\s*)*data_/i.test(text.slice(0, 2048)))\n  const parsed = cif ? parseMmcif(text) : parsePdb(text)\n  const raw = parsed.atoms.filter((atom) =>\n    Number.isFinite(atom.x + atom.y + atom.z)\n  )\n  // Keep one conformer per atom: the first alternate location in the file.\n  const seen = new Map<string, string>()\n  const kept = raw.filter((atom) => {\n    if (!atom.altLoc) return true\n    const key = `${atom.chain}|${atom.seq}|${atom.insertion}|${atom.residueName}|${atom.name}`\n    const first = seen.get(key)\n    if (first === undefined) seen.set(key, atom.altLoc)\n    return (first ?? atom.altLoc) === atom.altLoc\n  })\n  if (!kept.length) throw new Error(\"The file contains no atom coordinates.\")\n\n  const residues: MoleculeResidue[] = []\n  const groups: RawAtom[][] = []\n  const byId = new Map<string, number>()\n  const atoms: MoleculeAtom[] = []\n  for (const atom of kept) {\n    const id = `${atom.chain}:${atom.seq}${atom.insertion}`\n    let index = byId.get(id)\n    if (index === undefined || residues[index]!.name !== atom.residueName) {\n      index = residues.length\n      byId.set(id, index)\n      residues.push({\n        id: residues.some((residue) => residue.id === id)\n          ? `${id}:${atom.residueName}`\n          : id,\n        name: atom.residueName,\n        chain: atom.chain,\n        seq: atom.seq,\n        insertion: atom.insertion,\n        kind: \"ligand\",\n        secondary: \"coil\",\n        atoms: [],\n      })\n      groups.push([])\n    }\n    residues[index]!.atoms.push(atoms.length)\n    groups[index]!.push(atom)\n    atoms.push({\n      serial: atom.serial,\n      name: atom.name,\n      element: atom.element || \"C\",\n      x: atom.x,\n      y: atom.y,\n      z: atom.z,\n      residue: index,\n    })\n  }\n  residues.forEach((residue, index) => {\n    residue.kind = residueKind(residue.name, groups[index]!)\n  })\n  for (const range of parsed.ranges) {\n    const chain = residues.filter(\n      ({ chain, kind }) => chain === range.chain && kind === \"protein\"\n    )\n    const label = ({ seq, insertion }: MoleculeResidue) => `${seq}${insertion}`\n    const start = chain.findIndex((residue) => label(residue) === range.start)\n    const end = chain.findIndex((residue) => label(residue) === range.end)\n    if (start < 0 || end < start) continue\n    for (const residue of chain.slice(start, end + 1))\n      residue.secondary = range.kind\n  }\n  return {\n    id: parsed.id,\n    title: parsed.title,\n    atoms,\n    residues,\n    chains: [...new Set(residues.map(({ chain }) => chain))],\n    bonds: inferBonds(atoms, residues),\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Appearance\n\nconst STAGE_TONE = new Color(\"#e4e3dc\")\nconst CHAIN_COLORS = [\n  \"#4e79a7\",\n  \"#e15759\",\n  \"#59a14f\",\n  \"#f28e2b\",\n  \"#b07aa1\",\n  \"#76b7b2\",\n  \"#edc948\",\n  \"#ff9da7\",\n  \"#9c755f\",\n  \"#bab0ac\",\n]\nconst SECONDARY_COLORS: Record<SecondaryStructure, string> = {\n  helix: \"#c8466d\",\n  sheet: \"#e0a43a\",\n  coil: \"#a3a9b0\",\n}\n// Jmol's CPK palette, with carbon darkened to stand out on a light stage.\nconst ELEMENT_COLORS: Record<string, string> = {\n  H: \"#f2f2f2\",\n  C: \"#8c8c8c\",\n  N: \"#3050f8\",\n  O: \"#ff0d0d\",\n  S: \"#e6c228\",\n  P: \"#ff8000\",\n  Se: \"#ffa100\",\n  Fe: \"#e06633\",\n  Zn: \"#7d80b0\",\n  Mg: \"#8aff00\",\n  Ca: \"#3dff00\",\n  Na: \"#ab5cf2\",\n  K: \"#8f40d4\",\n  Cl: \"#1ff01f\",\n  Cu: \"#c88033\",\n  Mn: \"#9c7ac7\",\n  F: \"#90e050\",\n  Br: \"#a62929\",\n  I: \"#940094\",\n}\nconst RESIDUE_GROUPS: [string, string, string[]][] = [\n  [\n    \"Hydrophobic\",\n    \"#d9a441\",\n    [\"ALA\", \"VAL\", \"LEU\", \"ILE\", \"MET\", \"MSE\", \"PHE\", \"TRP\"],\n  ],\n  [\"Polar\", \"#4fa67a\", [\"SER\", \"THR\", \"ASN\", \"GLN\", \"TYR\", \"CYS\", \"SEC\"]],\n  [\"Acidic\", \"#d1495b\", [\"ASP\", \"GLU\"]],\n  [\"Basic\", \"#3f7cc0\", [\"LYS\", \"ARG\", \"HIS\", \"PYL\"]],\n  [\"Glycine and proline\", \"#9b8bc4\", [\"GLY\", \"PRO\"]],\n]\nconst BASE_COLORS: Record<string, string> = {\n  A: \"#e15759\",\n  T: \"#4e79a7\",\n  U: \"#4e79a7\",\n  G: \"#59a14f\",\n  C: \"#edc948\",\n  I: \"#a3a9b0\",\n}\nconst OTHER = \"#a3a9b0\"\n\n// Van der Waals radii in ångströms (Bondi), for spacefill.\nconst VDW: Record<string, number> = {\n  H: 1.1,\n  C: 1.7,\n  N: 1.55,\n  O: 1.52,\n  S: 1.8,\n  P: 1.8,\n  Se: 1.9,\n  F: 1.47,\n  Cl: 1.75,\n  Br: 1.85,\n  I: 1.98,\n}\n\nfunction residueColor(\n  structure: MolecularStructure,\n  index: number,\n  scheme: MoleculeColorScheme,\n  rainbow: Map<number, number>\n) {\n  const residue = structure.residues[index]!\n  switch (scheme) {\n    case \"secondary\":\n      return residue.kind === \"nucleic\"\n        ? (BASE_COLORS[NUCLEOTIDES[residue.name] ?? \"\"] ?? OTHER)\n        : SECONDARY_COLORS[residue.secondary]\n    case \"residue\":\n      if (residue.kind === \"nucleic\")\n        return BASE_COLORS[NUCLEOTIDES[residue.name] ?? \"\"] ?? OTHER\n      return (\n        RESIDUE_GROUPS.find(([, , names]) =>\n          names.includes(residue.name)\n        )?.[1] ?? OTHER\n      )\n    case \"rainbow\": {\n      const position = rainbow.get(index)\n      return position === undefined\n        ? OTHER\n        : `#${new Color().setHSL((1 - position) * 0.68, 0.7, 0.5).getHexString()}`\n    }\n    default:\n      return CHAIN_COLORS[\n        structure.chains.indexOf(residue.chain) % CHAIN_COLORS.length\n      ]!\n  }\n}\n\nfunction atomColor(\n  structure: MolecularStructure,\n  index: number,\n  scheme: MoleculeColorScheme,\n  rainbow: Map<number, number>\n) {\n  const atom = structure.atoms[index]!\n  const residue = structure.residues[atom.residue]!\n  // Ligands and heteroatoms read best in element colours whatever the scheme.\n  if (\n    scheme === \"element\" ||\n    residue.kind === \"ligand\" ||\n    residue.kind === \"water\" ||\n    (atom.element !== \"C\" && scheme !== \"chain\" && scheme !== \"rainbow\")\n  )\n    return ELEMENT_COLORS[atom.element] ?? \"#ff1493\"\n  return residueColor(structure, atom.residue, scheme, rainbow)\n}\n\n/** Display name for a residue, such as `Cys 16 · chain A`. */\nexport function residueLabel(residue: MoleculeResidue) {\n  const name =\n    residue.name.length === 3\n      ? residue.name[0] + residue.name.slice(1).toLowerCase()\n      : residue.name\n  const chain = residue.chain ? ` · chain ${residue.chain}` : \"\"\n  return `${name} ${residue.seq}${residue.insertion}${chain}`\n}\n\n/** One-letter code for a polymer residue, or the ligand code. */\nexport function residueCode(residue: MoleculeResidue) {\n  if (residue.kind === \"protein\") return AMINO_ACIDS[residue.name] ?? \"X\"\n  if (residue.kind === \"nucleic\") return NUCLEOTIDES[residue.name] ?? \"N\"\n  return residue.name\n}\n\n// ---------------------------------------------------------------------------\n// Geometry\n\ntype Built = {\n  group: Group\n  /** Residue index for a raycast hit on one of `pickable`. */\n  pick: (hit: Intersection<Object3D>) => number | undefined\n  pickable: Object3D[]\n  paint: (atomColors: Color[], residueColors: Color[], selected: number) => void\n  dispose: () => void\n}\n\nconst UP = new Vector3(0, 1, 0)\n\nfunction material() {\n  return new MeshStandardMaterial({ roughness: 0.55, metalness: 0 })\n}\n\n// Instanced spheres and half-bond cylinders for a set of atoms.\nfunction atomMeshes(\n  structure: MolecularStructure,\n  atomIndices: number[],\n  style: \"ball-and-stick\" | \"spacefill\"\n) {\n  const { atoms, bonds } = structure\n  const detail = atomIndices.length > 20_000 ? [8, 6] : [16, 12]\n  const sphereGeometry = new SphereGeometry(1, detail[0], detail[1])\n  const spheres = new InstancedMesh(\n    sphereGeometry,\n    material(),\n    Math.max(1, atomIndices.length)\n  )\n  spheres.count = atomIndices.length\n  const matrix = new Matrix4()\n  const position = new Vector3()\n  const quaternion = new Quaternion()\n  const scale = new Vector3()\n  atomIndices.forEach((index, instance) => {\n    const atom = atoms[index]!\n    const radius =\n      style === \"spacefill\"\n        ? (VDW[atom.element] ?? 1.7)\n        : atom.element === \"H\"\n          ? 0.16\n          : 0.28\n    matrix.compose(\n      position.set(atom.x, atom.y, atom.z),\n      quaternion.identity(),\n      scale.setScalar(radius)\n    )\n    spheres.setMatrixAt(instance, matrix)\n  })\n  const halves: number[] = []\n  if (style === \"ball-and-stick\") {\n    const shown = new Set(atomIndices)\n    for (let i = 0; i < bonds.length; i += 2)\n      if (shown.has(bonds[i]!) && shown.has(bonds[i + 1]!))\n        halves.push(bonds[i]!, bonds[i + 1]!, bonds[i + 1]!, bonds[i]!)\n  }\n  const sticks = new InstancedMesh(\n    new CylinderGeometry(1, 1, 1, 8, 1, true),\n    material(),\n    Math.max(1, halves.length / 2)\n  )\n  sticks.count = halves.length / 2\n  const a = new Vector3()\n  const b = new Vector3()\n  const direction = new Vector3()\n  for (let i = 0; i < halves.length; i += 2) {\n    const from = atoms[halves[i]!]!\n    const to = atoms[halves[i + 1]!]!\n    a.set(from.x, from.y, from.z)\n    b.set(to.x, to.y, to.z).add(a).multiplyScalar(0.5)\n    direction.subVectors(b, a)\n    const length = direction.length()\n    matrix.compose(\n      position.addVectors(a, b).multiplyScalar(0.5),\n      quaternion.setFromUnitVectors(UP, direction.normalize()),\n      scale.set(0.12, length, 0.12)\n    )\n    sticks.setMatrixAt(i / 2, matrix)\n  }\n  const color = new Color()\n  // Instance colours must exist before the first render compiles the shader.\n  for (let i = 0; i < spheres.count; i++) spheres.setColorAt(i, color)\n  for (let i = 0; i < sticks.count; i++) sticks.setColorAt(i, color)\n  spheres.computeBoundingSphere()\n  sticks.computeBoundingSphere()\n  return {\n    spheres,\n    sticks,\n    atomOf(object: Object3D, instance: number) {\n      if (object === spheres) return atomIndices[instance]\n      if (object === sticks) return halves[instance * 2]\n    },\n    paint(colors: Color[]) {\n      atomIndices.forEach((index, instance) =>\n        spheres.setColorAt(instance, colors[index]!)\n      )\n      for (let i = 0; i < halves.length; i += 2)\n        sticks.setColorAt(i / 2, colors[halves[i]!]!)\n      if (spheres.instanceColor) spheres.instanceColor.needsUpdate = true\n      if (sticks.instanceColor) sticks.instanceColor.needsUpdate = true\n    },\n    dispose() {\n      for (const mesh of [spheres, sticks]) {\n        mesh.geometry.dispose()\n        ;(mesh.material as MeshStandardMaterial).dispose()\n        mesh.dispose()\n      }\n    },\n  }\n}\n\ntype Segment = { residues: number[]; points: Vector3[]; guides: Vector3[] }\n\n// Polymer runs whose trace atoms (CA, or P for nucleic acids) are bonded.\nfunction backboneSegments(structure: MolecularStructure) {\n  const segments: Segment[] = []\n  let current: Segment | null = null\n  let previous: MoleculeResidue | null = null\n  structure.residues.forEach((residue, index) => {\n    const find = (...names: string[]) => {\n      for (const name of names) {\n        const atom = residue.atoms\n          .map((i) => structure.atoms[i]!)\n          .find((atom) => atom.name === name)\n        if (atom) return new Vector3(atom.x, atom.y, atom.z)\n      }\n    }\n    const protein = residue.kind === \"protein\"\n    const trace = protein\n      ? find(\"CA\")\n      : residue.kind === \"nucleic\"\n        ? find(\"P\", \"C4'\")\n        : undefined\n    if (!trace) {\n      current = null\n      previous = null\n      return\n    }\n    const last: Vector3 | undefined = current?.points.at(-1)\n    const gap = protein ? 4.3 : 8.5\n    if (\n      !current ||\n      !previous ||\n      previous.chain !== residue.chain ||\n      previous.kind !== residue.kind ||\n      !last ||\n      last.distanceTo(trace) > gap\n    ) {\n      current = { residues: [], points: [], guides: [] }\n      segments.push(current)\n    }\n    // The ribbon's wide side follows the peptide's C=O, or the base's\n    // direction from the backbone.\n    const from = protein ? find(\"C\") : trace\n    const to = protein ? find(\"O\") : find(\"C1'\", \"N1\", \"N9\")\n    const guide =\n      from && to ? to.clone().sub(from).normalize() : new Vector3(0, 0, 1)\n    const before = current.guides.at(-1)\n    if (before && guide.dot(before) < 0) guide.negate()\n    current.residues.push(index)\n    current.points.push(trace)\n    current.guides.push(guide)\n    previous = residue\n  })\n  return segments.filter((segment) => segment.points.length > 1)\n}\n\nfunction cartoonGeometry(structure: MolecularStructure) {\n  const segments = backboneSegments(structure)\n  const total = segments.reduce((sum, { points }) => sum + points.length, 0)\n  const steps = total > 3000 ? 3 : 6\n  const sides = 10\n  const positions: number[] = []\n  const normals: number[] = []\n  const owners: number[] = []\n  const indices: number[] = []\n  const shape = (residue: MoleculeResidue) =>\n    residue.kind === \"nucleic\"\n      ? [0.6, 0.6]\n      : residue.secondary === \"helix\"\n        ? [1.3, 0.25]\n        : residue.secondary === \"sheet\"\n          ? [1.1, 0.25]\n          : [0.3, 0.3]\n  const tangent = new Vector3()\n  const normal = new Vector3()\n  const binormal = new Vector3()\n  const offset = new Vector3()\n  const surface = new Vector3()\n  for (const segment of segments) {\n    const curve = new CatmullRomCurve3(segment.points, false, \"centripetal\")\n    const count = segment.points.length\n    const samples = (count - 1) * steps\n    const shapes = segment.residues.map((i) => shape(structure.residues[i]!))\n    const base = positions.length / 3\n    for (let s = 0; s <= samples; s++) {\n      const t = s / samples\n      const along = s / steps\n      const low = Math.min(count - 1, Math.floor(along))\n      const high = Math.min(count - 1, low + 1)\n      const mix = along - low\n      const point = curve.getPoint(t)\n      curve.getTangent(t, tangent)\n      normal.copy(segment.guides[low]!).lerp(segment.guides[high]!, mix)\n      normal.sub(offset.copy(tangent).multiplyScalar(normal.dot(tangent)))\n      if (normal.lengthSq() < 1e-6) normal.set(0, 0, 1).cross(tangent)\n      normal.normalize()\n      binormal.crossVectors(tangent, normal).normalize()\n      // Shapes ease between residues so helices and strands taper.\n      const ease = mix * mix * (3 - 2 * mix)\n      const width =\n        shapes[low]![0]! + (shapes[high]![0]! - shapes[low]![0]!) * ease\n      const thickness =\n        shapes[low]![1]! + (shapes[high]![1]! - shapes[low]![1]!) * ease\n      const owner = segment.residues[Math.round(along)]!\n      for (let k = 0; k < sides; k++) {\n        const angle = (k / sides) * Math.PI * 2\n        const cos = Math.cos(angle)\n        const sin = Math.sin(angle)\n        surface\n          .copy(normal)\n          .multiplyScalar(cos * width)\n          .addScaledVector(binormal, sin * thickness)\n          .add(point)\n        positions.push(surface.x, surface.y, surface.z)\n        offset\n          .copy(normal)\n          .multiplyScalar(cos / width)\n          .addScaledVector(binormal, sin / thickness)\n          .normalize()\n        normals.push(offset.x, offset.y, offset.z)\n        owners.push(owner)\n      }\n    }\n    for (let s = 0; s < samples; s++)\n      for (let k = 0; k < sides; k++) {\n        const a = base + s * sides + k\n        const b = base + s * sides + ((k + 1) % sides)\n        const c = a + sides\n        const d = b + sides\n        indices.push(a, c, b, b, c, d)\n      }\n  }\n  const geometry = new BufferGeometry()\n  geometry.setAttribute(\n    \"position\",\n    new BufferAttribute(new Float32Array(positions), 3)\n  )\n  geometry.setAttribute(\n    \"normal\",\n    new BufferAttribute(new Float32Array(normals), 3)\n  )\n  geometry.setAttribute(\n    \"color\",\n    new BufferAttribute(new Float32Array(positions.length), 3)\n  )\n  geometry.setIndex(indices)\n  geometry.computeBoundingSphere()\n  return { geometry, owners }\n}\n\nfunction build(\n  structure: MolecularStructure,\n  representation: MoleculeRepresentation,\n  water: boolean\n): Built {\n  const group = new Group()\n  const visible = (index: number) =>\n    water ||\n    structure.residues[structure.atoms[index]!.residue]!.kind !== \"water\"\n  const all = structure.atoms.map((_, i) => i).filter(visible)\n\n  if (representation !== \"cartoon\") {\n    const meshes = atomMeshes(structure, all, representation)\n    group.add(meshes.spheres, meshes.sticks)\n    return {\n      group,\n      pickable: [meshes.spheres, meshes.sticks],\n      pick(hit) {\n        const atom =\n          hit.instanceId === undefined\n            ? undefined\n            : meshes.atomOf(hit.object, hit.instanceId)\n        return atom === undefined ? undefined : structure.atoms[atom]!.residue\n      },\n      paint: (colors) => meshes.paint(colors),\n      dispose: () => meshes.dispose(),\n    }\n  }\n\n  const { geometry, owners } = cartoonGeometry(structure)\n  const ribbon = new Mesh(\n    geometry,\n    new MeshStandardMaterial({\n      roughness: 0.5,\n      metalness: 0,\n      vertexColors: true,\n    })\n  )\n  group.add(ribbon)\n  // Ligands, and residues no ribbon can show, keep their atoms.\n  const traced = new Set(owners)\n  const extra = all.filter((index) => {\n    const residue = structure.atoms[index]!.residue\n    return !traced.has(residue)\n  })\n  const others = atomMeshes(structure, extra, \"ball-and-stick\")\n  group.add(others.spheres, others.sticks)\n  let detail: ReturnType<typeof atomMeshes> | null = null\n  let detailFor = -1\n  const colorAttribute = geometry.getAttribute(\"color\") as BufferAttribute\n  return {\n    group,\n    get pickable() {\n      return [\n        ribbon,\n        others.spheres,\n        others.sticks,\n        ...(detail ? [detail.spheres, detail.sticks] : []),\n      ]\n    },\n    pick(hit) {\n      if (hit.object === ribbon)\n        return hit.face ? owners[hit.face.a] : undefined\n      if (hit.instanceId === undefined) return\n      const atom =\n        others.atomOf(hit.object, hit.instanceId) ??\n        detail?.atomOf(hit.object, hit.instanceId)\n      return atom === undefined ? undefined : structure.atoms[atom]!.residue\n    },\n    paint(atomColors, residueColors, selected) {\n      owners.forEach((owner, vertex) => {\n        const color = residueColors[owner]!\n        colorAttribute.setXYZ(vertex, color.r, color.g, color.b)\n      })\n      colorAttribute.needsUpdate = true\n      others.paint(atomColors)\n      // A selected polymer residue also shows its atoms on the ribbon.\n      if (selected !== detailFor) {\n        if (detail) {\n          group.remove(detail.spheres, detail.sticks)\n          detail.dispose()\n          detail = null\n        }\n        detailFor = selected\n        if (selected >= 0 && traced.has(selected)) {\n          detail = atomMeshes(\n            structure,\n            structure.residues[selected]!.atoms.filter(visible),\n            \"ball-and-stick\"\n          )\n          group.add(detail.spheres, detail.sticks)\n        }\n      }\n      detail?.paint(atomColors)\n    },\n    dispose() {\n      geometry.dispose()\n      ;(ribbon.material as MeshStandardMaterial).dispose()\n      others.dispose()\n      detail?.dispose()\n    },\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Component\n\ntype Engine = MoleculeViewerHandle & {\n  show: (representation: MoleculeRepresentation, water: boolean) => void\n  paint: (scheme: MoleculeColorScheme, selected: number) => void\n}\n\nconst iconProps = {\n  width: 16,\n  height: 16,\n  viewBox: \"0 0 24 24\",\n  fill: \"none\",\n  stroke: \"currentColor\",\n  strokeWidth: 2,\n  strokeLinecap: \"round\" as const,\n  strokeLinejoin: \"round\" as const,\n  \"aria-hidden\": true,\n}\n\nfunction ToolbarButton({\n  label,\n  onClick,\n  children,\n}: {\n  label: string\n  onClick: () => void\n  children: React.ReactNode\n}) {\n  return (\n    <Tooltip>\n      <TooltipTrigger\n        render={\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            aria-label={label}\n            onClick={onClick}\n          />\n        }\n      >\n        {children}\n      </TooltipTrigger>\n      <TooltipContent side=\"bottom\">{label}</TooltipContent>\n    </Tooltip>\n  )\n}\n\nfunction SequenceStrip({\n  structure,\n  water,\n  selected,\n  onSelect,\n}: {\n  structure: MolecularStructure\n  water: boolean\n  selected: number\n  onSelect: (index: number) => void\n}) {\n  const strip = React.useRef<HTMLDivElement>(null)\n  const chains = React.useMemo(\n    () =>\n      structure.chains\n        .map((chain) => ({\n          chain,\n          residues: structure.residues\n            .map((residue, index) => ({ residue, index }))\n            .filter(\n              ({ residue }) =>\n                residue.chain === chain && (water || residue.kind !== \"water\")\n            ),\n        }))\n        .filter(({ residues }) => residues.length),\n    [structure, water]\n  )\n  const order = React.useMemo(\n    () => chains.flatMap(({ residues }) => residues.map(({ index }) => index)),\n    [chains]\n  )\n  const [focus, setFocus] = React.useState<number | null>(null)\n  const stop =\n    focus !== null && order.includes(focus)\n      ? focus\n      : order.includes(selected)\n        ? selected\n        : order[0]\n\n  // Scroll a selection made on the stage into view inside the strip only, so\n  // the page never jumps.\n  React.useEffect(() => {\n    const container = strip.current\n    const button = container?.querySelector<HTMLElement>(\n      `[data-residue=\"${selected}\"]`\n    )\n    if (!container || !button) return\n    const left = button.offsetLeft - container.offsetLeft\n    if (\n      left < container.scrollLeft ||\n      left + button.offsetWidth > container.scrollLeft + container.clientWidth\n    )\n      container.scrollLeft = left - container.clientWidth / 2\n  }, [selected])\n\n  const move = (index: number) => {\n    setFocus(index)\n    strip.current\n      ?.querySelector<HTMLElement>(`[data-residue=\"${index}\"]`)\n      ?.focus()\n  }\n\n  return (\n    <div\n      ref={strip}\n      role=\"toolbar\"\n      aria-label=\"Sequence\"\n      className=\"flex shrink-0 gap-3 overflow-x-auto border-t bg-background/60 px-2 py-1.5 font-mono text-xs\"\n      onKeyDown={(event) => {\n        const at = order.indexOf(stop ?? -1)\n        const next: Record<string, number> = {\n          ArrowRight: Math.min(order.length - 1, at + 1),\n          ArrowLeft: Math.max(0, at - 1),\n          Home: 0,\n          End: order.length - 1,\n        }\n        const target = next[event.key]\n        if (target === undefined) return\n        event.preventDefault()\n        move(order[target]!)\n      }}\n    >\n      {chains.map(({ chain, residues }) => (\n        <div key={chain} className=\"flex items-center gap-px\">\n          {structure.chains.length > 1 && (\n            <span className=\"pr-1 text-muted-foreground\">{chain}</span>\n          )}\n          {residues.map(({ residue, index }) => {\n            const code = residueCode(residue)\n            return (\n              <button\n                key={index}\n                type=\"button\"\n                data-residue={index}\n                tabIndex={index === stop ? 0 : -1}\n                aria-pressed={index === selected}\n                aria-label={residueLabel(residue)}\n                title={residueLabel(residue)}\n                onFocus={() => setFocus(index)}\n                onClick={() => onSelect(index)}\n                className={cn(\n                  \"min-w-5 rounded px-0.5 py-0.5 text-center hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring\",\n                  code.length > 1 && \"px-1 text-muted-foreground\",\n                  index === selected &&\n                    \"bg-primary text-primary-foreground hover:bg-primary\"\n                )}\n              >\n                {code}\n              </button>\n            )\n          })}\n        </div>\n      ))}\n    </div>\n  )\n}\n\nfunction useStructure(\n  src: string | undefined,\n  data: string | undefined,\n  format: \"pdb\" | \"mmcif\" | undefined,\n  attempt: number\n): { structure?: MolecularStructure; error?: string } {\n  const fromData = React.useMemo(() => {\n    if (data === undefined) return null\n    try {\n      return { structure: parseStructure(data, format) }\n    } catch (reason) {\n      return {\n        error:\n          reason instanceof Error\n            ? reason.message\n            : \"The file could not be read.\",\n      }\n    }\n  }, [data, format])\n  const request = `${attempt}:${src}:${format}`\n  const [fetched, setFetched] = React.useState<{\n    request: string\n    structure?: MolecularStructure\n    error?: string\n  } | null>(null)\n  React.useEffect(() => {\n    if (data !== undefined || !src) return\n    const abort = new AbortController()\n    const timeout = setTimeout(() => abort.abort(\"timeout\"), 60_000)\n    fetch(src, { signal: abort.signal })\n      .then((response) => {\n        if (!response.ok)\n          throw new Error(\n            `The structure request returned HTTP ${response.status}.`\n          )\n        return response.text()\n      })\n      .then((text) =>\n        setFetched({ request, structure: parseStructure(text, format) })\n      )\n      .catch((reason: unknown) => {\n        if (abort.signal.aborted && abort.signal.reason !== \"timeout\") return\n        setFetched({\n          request,\n          error:\n            abort.signal.reason === \"timeout\"\n              ? \"The download timed out.\"\n              : reason instanceof Error\n                ? reason.message\n                : \"The file could not be read.\",\n        })\n      })\n      .finally(() => clearTimeout(timeout))\n    return () => {\n      abort.abort(\"unmount\")\n      clearTimeout(timeout)\n    }\n  }, [src, data, format, request])\n  if (fromData) return fromData\n  if (!src) return { error: \"No structure was given.\" }\n  return fetched?.request === request ? fetched : {}\n}\n\nexport function MoleculeViewer({\n  src,\n  data,\n  format,\n  representation: controlledRepresentation,\n  defaultRepresentation = \"cartoon\",\n  onRepresentationChange,\n  colorScheme: controlledScheme,\n  defaultColorScheme = \"secondary\",\n  onColorSchemeChange,\n  selectedId = null,\n  onSelectionChange,\n  onLoad,\n  water = false,\n  sequence = true,\n  controls = true,\n  attribution = true,\n  label = \"Interactive molecular structure\",\n  className,\n  ref,\n}: MoleculeViewerProps) {\n  const host = React.useRef<HTMLDivElement>(null)\n  const [attempt, setAttempt] = React.useState(0)\n  const { structure = null, error = null } = useStructure(\n    src,\n    data,\n    format,\n    attempt\n  )\n  const [graphicsFailed, setGraphicsFailed] = React.useState(false)\n  const [engine, setEngine] = React.useState<Engine | null>(null)\n  const [interacted, setInteracted] = React.useState(false)\n  const [ownRepresentation, setOwnRepresentation] = React.useState(\n    defaultRepresentation\n  )\n  const [ownScheme, setOwnScheme] = React.useState(defaultColorScheme)\n  const representation = controlledRepresentation ?? ownRepresentation\n  const scheme = controlledScheme ?? ownScheme\n  const instructions = React.useId()\n  const latest = React.useRef({ onSelectionChange, onLoad, structure })\n  React.useLayoutEffect(() => {\n    latest.current = { onSelectionChange, onLoad, structure }\n  })\n\n  const selected = React.useMemo(\n    () =>\n      structure && selectedId\n        ? structure.residues.findIndex(({ id }) => id === selectedId)\n        : -1,\n    [structure, selectedId]\n  )\n  const select = React.useCallback((index: number) => {\n    const residue = latest.current.structure?.residues[index] ?? null\n    latest.current.onSelectionChange?.(residue?.id ?? null, residue)\n  }, [])\n\n  React.useEffect(() => {\n    if (structure) latest.current.onLoad?.(structure)\n  }, [structure])\n\n  React.useEffect(() => {\n    const container = host.current\n    if (!structure || !container || graphicsFailed) return\n    const canvas = document.createElement(\"canvas\")\n    canvas.className = \"block size-full touch-none outline-none\"\n    canvas.setAttribute(\"aria-hidden\", \"true\")\n    container.prepend(canvas)\n    let renderer: WebGLRenderer\n    try {\n      renderer = new WebGLRenderer({\n        canvas,\n        antialias: true,\n        alpha: true,\n        powerPreference: \"low-power\",\n      })\n    } catch {\n      canvas.remove()\n      // WebGL is an external system; its failure is reported once per attempt.\n      // eslint-disable-next-line react-hooks/set-state-in-effect\n      setGraphicsFailed(true)\n      return\n    }\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))\n    const stage = new Scene()\n    stage.add(new AmbientLight(\"#ffffff\", 0.8))\n    stage.add(new HemisphereLight(\"#fffcf3\", \"#a49b8f\", 1.4))\n    const lights: [number[], number, string][] = [\n      [[-4, 6, 6], 2.4, \"#fff5e6\"],\n      [[5, 1, -4], 1.6, \"#e1edff\"],\n    ]\n    const camera = new PerspectiveCamera(34, 1, 0.1, 1000)\n    for (const [position, intensity, color] of lights) {\n      const light = new DirectionalLight(color, intensity)\n      light.position.set(position[0]!, position[1]!, position[2]!)\n      // Lights follow the camera so every side of the molecule is lit.\n      camera.add(light)\n    }\n    stage.add(camera)\n    const orbit = new OrbitControls(camera, canvas)\n    orbit.enablePan = false\n    orbit.rotateSpeed = 0.65\n    orbit.zoomSpeed = 0.8\n    const motion = matchMedia(\"(prefers-reduced-motion: reduce)\")\n    const applyMotion = () => (orbit.enableDamping = !motion.matches)\n    applyMotion()\n    motion.addEventListener(\"change\", applyMotion)\n\n    // Frame the polymer and ligands; water can spread far around them.\n    const framed = structure.atoms.filter(\n      (atom) => structure.residues[atom.residue]!.kind !== \"water\"\n    )\n    const points = (framed.length ? framed : structure.atoms).map(\n      ({ x, y, z }) => new Vector3(x, y, z)\n    )\n    const center = points\n      .reduce((sum, point) => sum.add(point), new Vector3())\n      .divideScalar(points.length)\n    const radius =\n      Math.max(...points.map((point) => point.distanceTo(center))) + 2\n    camera.near = radius / 100\n    camera.far = radius * 20\n    orbit.minDistance = 4\n    orbit.maxDistance = radius * 8\n    orbit.target.copy(center)\n\n    let dirty = true\n    const render = () => (dirty = true)\n    let frame = 0\n    const tick = () => {\n      if (orbit.update() || dirty) {\n        renderer.render(stage, camera)\n        dirty = false\n      }\n      frame = requestAnimationFrame(tick)\n    }\n\n    const reset = () => {\n      orbit.reset()\n      const halfV = (camera.fov * Math.PI) / 360\n      const halfH = Math.atan(Math.tan(halfV) * camera.aspect)\n      const distance = (radius / Math.sin(Math.min(halfV, halfH))) * 0.9\n      orbit.target.copy(center)\n      camera.position.set(center.x, center.y, center.z + distance)\n      camera.lookAt(center)\n      orbit.update()\n      render()\n    }\n    const resize = () => {\n      const width = container.clientWidth\n      const height = container.clientHeight\n      if (!width || !height) return\n      renderer.setSize(width, height, false)\n      camera.aspect = width / height\n      camera.updateProjectionMatrix()\n      render()\n    }\n    const observer = new ResizeObserver(resize)\n    observer.observe(container)\n    resize()\n    camera.aspect = container.clientWidth / Math.max(1, container.clientHeight)\n    reset()\n\n    let interacting = false\n    const onStart = () => (interacting = true)\n    const onEnd = () => (interacting = false)\n    const onChange = () => {\n      if (interacting) setInteracted(true)\n      render()\n    }\n    orbit.addEventListener(\"start\", onStart)\n    orbit.addEventListener(\"end\", onEnd)\n    orbit.addEventListener(\"change\", onChange)\n\n    let built: Built | null = null\n    const rainbow = new Map<number, number>()\n    for (const chain of structure.chains) {\n      const polymer = structure.residues\n        .map((residue, index) => ({ residue, index }))\n        .filter(\n          ({ residue }) =>\n            residue.chain === chain &&\n            (residue.kind === \"protein\" || residue.kind === \"nucleic\")\n        )\n      polymer.forEach(({ index }, position) =>\n        rainbow.set(index, position / Math.max(1, polymer.length - 1))\n      )\n    }\n\n    const raycaster = new Raycaster()\n    const down = new Vector2()\n    const onPointerDown = (event: PointerEvent) =>\n      down.set(event.clientX, event.clientY)\n    const onPointerUp = (event: PointerEvent) => {\n      if (!built) return\n      if (down.distanceTo(new Vector2(event.clientX, event.clientY)) > 4) return\n      const bounds = canvas.getBoundingClientRect()\n      raycaster.setFromCamera(\n        new Vector2(\n          ((event.clientX - bounds.left) / bounds.width) * 2 - 1,\n          -((event.clientY - bounds.top) / bounds.height) * 2 + 1\n        ),\n        camera\n      )\n      const hit = raycaster.intersectObjects(built.pickable, false)[0]\n      const residue = hit && built.pick(hit)\n      if (residue !== undefined) select(residue)\n    }\n    canvas.addEventListener(\"pointerdown\", onPointerDown)\n    canvas.addEventListener(\"pointerup\", onPointerUp)\n    const onContextLost = (event: Event) => {\n      event.preventDefault()\n      setGraphicsFailed(true)\n    }\n    canvas.addEventListener(\"webglcontextlost\", onContextLost)\n\n    setEngine({\n      show(representation, water) {\n        if (built) {\n          stage.remove(built.group)\n          built.dispose()\n        }\n        built = build(structure, representation, water)\n        stage.add(built.group)\n        render()\n      },\n      paint(scheme, selected) {\n        if (!built) return\n        // Everything but the selected residue fades towards the stage.\n        const dim = (hex: string, residue: number) => {\n          const color = new Color(hex)\n          return selected >= 0 && residue !== selected\n            ? color.lerp(STAGE_TONE, 0.65)\n            : color\n        }\n        const residueColors = structure.residues.map((_, index) =>\n          dim(residueColor(structure, index, scheme, rainbow), index)\n        )\n        const atomColors = structure.atoms.map((atom, index) =>\n          dim(atomColor(structure, index, scheme, rainbow), atom.residue)\n        )\n        built.paint(atomColors, residueColors, selected)\n        render()\n      },\n      reset,\n      zoom(factor) {\n        const offset = camera.position.clone().sub(orbit.target)\n        offset.setLength(\n          Math.min(\n            orbit.maxDistance,\n            Math.max(orbit.minDistance, offset.length() * factor)\n          )\n        )\n        camera.position.copy(orbit.target).add(offset)\n        orbit.update()\n        setInteracted(true)\n        render()\n      },\n      rotate(azimuth, polar = 0) {\n        const spherical = new Spherical().setFromVector3(\n          camera.position.clone().sub(orbit.target)\n        )\n        spherical.theta += azimuth\n        spherical.phi += polar\n        spherical.makeSafe()\n        camera.position\n          .copy(orbit.target)\n          .add(new Vector3().setFromSpherical(spherical))\n        orbit.update()\n        setInteracted(true)\n        render()\n      },\n    })\n    frame = requestAnimationFrame(tick)\n\n    return () => {\n      cancelAnimationFrame(frame)\n      setEngine(null)\n      observer.disconnect()\n      motion.removeEventListener(\"change\", applyMotion)\n      canvas.removeEventListener(\"pointerdown\", onPointerDown)\n      canvas.removeEventListener(\"pointerup\", onPointerUp)\n      canvas.removeEventListener(\"webglcontextlost\", onContextLost)\n      orbit.dispose()\n      if (built) built.dispose()\n      renderer.dispose()\n      canvas.remove()\n    }\n  }, [structure, graphicsFailed, select])\n\n  React.useEffect(() => {\n    engine?.show(representation, water)\n  }, [engine, representation, water])\n\n  React.useEffect(() => {\n    engine?.paint(scheme, selected)\n  }, [engine, representation, water, scheme, selected])\n\n  React.useImperativeHandle(\n    ref,\n    () => ({\n      zoom: (factor) => engine?.zoom(factor),\n      rotate: (azimuth, polar) => engine?.rotate(azimuth, polar),\n      reset: () => engine?.reset(),\n    }),\n    [engine]\n  )\n\n  const retry = () => {\n    setGraphicsFailed(false)\n    setAttempt((n) => n + 1)\n  }\n  const message = error\n    ? {\n        title: \"The structure couldn’t load\",\n        text: `${error} Check the file and your connection, then try again.`,\n      }\n    : graphicsFailed\n      ? {\n          title: \"3D view unavailable\",\n          text: \"WebGL could not start or its connection was interrupted. Try again, or use a browser with hardware acceleration enabled.\",\n        }\n      : null\n  const residue =\n    structure && selected >= 0 ? structure.residues[selected]! : null\n  const status = residue\n    ? `${residueLabel(residue)}${\n        residue.kind === \"protein\" && residue.secondary !== \"coil\"\n          ? ` · ${residue.secondary}`\n          : \"\"\n      }`\n    : structure\n      ? // Titles in PDB files are upper case, so the summary leaves them out.\n        [\n          structure.id,\n          `${structure.residues.filter(({ kind }) => kind === \"protein\" || kind === \"nucleic\").length} residues`,\n          structure.chains.length > 1 && `${structure.chains.length} chains`,\n        ]\n          .filter(Boolean)\n          .join(\" · \")\n      : \"\"\n\n  return (\n    <div\n      className={cn(\n        \"flex h-[28rem] w-full flex-col overflow-hidden rounded-xl border bg-muted/30 text-sm text-foreground\",\n        className\n      )}\n    >\n      <div\n        ref={host}\n        role=\"region\"\n        aria-label={label}\n        aria-describedby={instructions}\n        tabIndex={0}\n        className=\"relative min-h-0 flex-1 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n        onKeyDown={(event) => {\n          if (event.target !== event.currentTarget || !engine) return\n          const actions: Record<string, () => void> = {\n            ArrowLeft: () => engine.rotate(-0.2),\n            ArrowRight: () => engine.rotate(0.2),\n            ArrowUp: () => engine.rotate(0, -0.2),\n            ArrowDown: () => engine.rotate(0, 0.2),\n            \"+\": () => engine.zoom(0.85),\n            \"=\": () => engine.zoom(0.85),\n            \"-\": () => engine.zoom(1.18),\n          }\n          const action = actions[event.key]\n          if (!action) return\n          event.preventDefault()\n          action()\n        }}\n      >\n        <p id={instructions} className=\"sr-only\">\n          Drag or use the arrow keys to rotate, and scroll, pinch or press plus\n          and minus to zoom. Click a residue to select it, or choose one from\n          the sequence.\n        </p>\n        {message ? (\n          <div className=\"absolute inset-0 flex items-center justify-center p-6\">\n            <Alert className=\"max-w-sm\">\n              <svg {...iconProps}>\n                <circle cx=\"12\" cy=\"12\" r=\"10\" />\n                <path d=\"M12 8v4M12 16h.01\" />\n              </svg>\n              <AlertTitle>{message.title}</AlertTitle>\n              <AlertDescription>\n                <p>{message.text}</p>\n                {src && data === undefined && (\n                  <Button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    onClick={retry}\n                    className=\"mt-3 text-foreground\"\n                  >\n                    Try again\n                  </Button>\n                )}\n              </AlertDescription>\n            </Alert>\n          </div>\n        ) : !structure ? (\n          <div\n            role=\"status\"\n            className=\"absolute inset-0 flex items-center justify-center p-6 text-muted-foreground\"\n          >\n            Loading structure\n          </div>\n        ) : null}\n        {engine && controls && (\n          <TooltipProvider>\n            <div\n              role=\"toolbar\"\n              aria-label=\"View controls\"\n              className=\"absolute top-2 right-2 left-2 flex flex-wrap items-center justify-end gap-0.5\"\n            >\n              <div className=\"flex flex-wrap items-center justify-end gap-0.5 rounded-lg border bg-background/90 p-1 shadow-sm backdrop-blur\">\n                <Select\n                  items={moleculeRepresentations}\n                  value={representation}\n                  onValueChange={(next) => {\n                    if (!next) return\n                    setOwnRepresentation(next)\n                    onRepresentationChange?.(next)\n                  }}\n                >\n                  <SelectTrigger\n                    aria-label=\"Style\"\n                    className=\"border-transparent\"\n                  >\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    <SelectGroup>\n                      {Object.entries(moleculeRepresentations).map(\n                        ([value, name]) => (\n                          <SelectItem key={value} value={value}>\n                            {name}\n                          </SelectItem>\n                        )\n                      )}\n                    </SelectGroup>\n                  </SelectContent>\n                </Select>\n                <Select\n                  items={moleculeColorSchemes}\n                  value={scheme}\n                  onValueChange={(next) => {\n                    if (!next) return\n                    setOwnScheme(next)\n                    onColorSchemeChange?.(next)\n                  }}\n                >\n                  <SelectTrigger\n                    aria-label=\"Colour\"\n                    className=\"border-transparent\"\n                  >\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    <SelectGroup>\n                      {Object.entries(moleculeColorSchemes).map(\n                        ([value, name]) => (\n                          <SelectItem key={value} value={value}>\n                            {name}\n                          </SelectItem>\n                        )\n                      )}\n                    </SelectGroup>\n                  </SelectContent>\n                </Select>\n                {/* Narrow stages pinch to zoom; the menus need the room. */}\n                <span className=\"hidden sm:contents\">\n                  <ToolbarButton\n                    label=\"Zoom in\"\n                    onClick={() => engine.zoom(0.85)}\n                  >\n                    <svg {...iconProps}>\n                      <path d=\"M5 12h14M12 5v14\" />\n                    </svg>\n                  </ToolbarButton>\n                  <ToolbarButton\n                    label=\"Zoom out\"\n                    onClick={() => engine.zoom(1.18)}\n                  >\n                    <svg {...iconProps}>\n                      <path d=\"M5 12h14\" />\n                    </svg>\n                  </ToolbarButton>\n                </span>\n                <ToolbarButton\n                  label=\"Reset view\"\n                  onClick={() => engine.reset()}\n                >\n                  <svg {...iconProps}>\n                    <path d=\"M3 12a9 9 0 1 0 3-6.7L3 8\" />\n                    <path d=\"M3 3v5h5\" />\n                  </svg>\n                </ToolbarButton>\n              </div>\n            </div>\n          </TooltipProvider>\n        )}\n        {engine && !interacted && (\n          <p\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-x-0 bottom-8 text-center text-xs text-muted-foreground\"\n          >\n            Drag to rotate · Scroll or pinch to zoom\n          </p>\n        )}\n        <div className=\"pointer-events-none absolute right-2 bottom-1.5 left-2 flex items-end justify-between gap-2 text-[11px] text-muted-foreground\">\n          <p aria-live=\"polite\" className=\"min-w-0 truncate\">\n            {status}\n          </p>\n          {attribution && structure?.id && (\n            <a\n              href={`https://www.rcsb.org/structure/${encodeURIComponent(structure.id)}`}\n              className=\"pointer-events-auto shrink-0 underline-offset-2 hover:underline\"\n            >\n              PDB {structure.id}\n            </a>\n          )}\n        </div>\n      </div>\n      {sequence && structure && (\n        <SequenceStrip\n          structure={structure}\n          water={water}\n          selected={selected}\n          onSelect={select}\n        />\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Molecule viewer docs: https://organui.com/docs/components/molecule-viewer. Needs one of shadcn's Base UI styles, such as base-nova: its controls use Base UI's render prop, so on a Radix style its buttons do nothing.",
  "categories": [
    "molecular"
  ],
  "type": "registry:component"
}