{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "anatomia",
  "title": "Anatomia",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "O núcleo das figuras didáticas: o web component <visant-anatomia> (pinos, painel de peças, passo a passo, pausa, tela cheia, teclado) e o wrapper React. A figura em si vem de um item anatomia-<nome>.",
  "files": [
    {
      "path": "registry/visant/lib/anatomia-tipos.ts",
      "content": "/**\n * Tipos da anatomia: o contrato entre o núcleo (`anatomia.ts`) e cada figura.\n * A figura nasce de `anatomia/figuras/<id>/geometria.mjs` (Node, na hora do\n * build) e ganha vida no `anatomia-<id>-controlador.ts` (navegador).\n */\n\nexport type Ponto3 = [number, number, number];\n\nexport interface Projecao {\n  origin: [number, number];\n  scale: number;\n  azimuth?: number;\n  elevation?: number;\n}\n\nexport interface Peca {\n  n: number;\n  nome: string;\n  texto: string;\n  ancora: Ponto3;\n  /** Rótulo do botão de ação, quando a peça faz alguma coisa. */\n  acao?: string;\n  /** Peças que trabalham com esta: ficam meio acesas quando ela está em foco. */\n  junto?: number[];\n}\n\nexport interface Passo {\n  titulo: string;\n  texto: string;\n  /** Peças que acendem neste passo. */\n  foco: number[];\n  /** O que o controlador precisa pra montar a cena do passo. */\n  pose?: unknown;\n}\n\nexport interface DadosDaFigura {\n  id: string;\n  fig: string;\n  titulo: string;\n  resumo: string;\n  width: number;\n  height: number;\n  svg: string;\n  P: Projecao;\n  modelo: Record<string, unknown>;\n  pecas: Peca[];\n  passos: Passo[];\n  leitura: string;\n  /** CSS só desta figura (estado de lâmpada, peça apagada...). Sem hex: tons do kit ou var() do site. */\n  css?: string;\n}\n\nexport interface AnatomiaApi {\n  /** O runtime do kit isométrico (iso, translateAlong, holdStill, axisVector...). */\n  kit: typeof import(\"./anatomia-kit\");\n  P: Projecao;\n  /** Escreve no canto de leitura (e no anúncio pra leitor de tela). */\n  dizer(texto: string): void;\n  /** Leva o pino da peça `n` pra um ponto do mundo (peça que se move). `null` esconde o pino (a peça saiu de cena). */\n  moverPino(n: number, ponto: Ponto3 | null): void;\n  /** Movimento reduzido: mostre a pose final, parada. */\n  reduzido(): boolean;\n  /** Laço de quadros. Só roda com a figura visível e sem pausa. `tick` devolve true pra continuar. */\n  loop(tick: (dt: number) => boolean): { iniciar(): void; parar(): void };\n  /** Mostra ou esconde o pino (e a peça some do ímã do foco). */\n  mostrarPino(n: number, visivel: boolean): void;\n  /** Troca o texto da peça no painel (quando ele depende do estado da cena). */\n  textoDaPeca(n: number, texto: string): void;\n  /** Relê rótulos e estado das ações (chame depois de mudar o que a ação faria). */\n  atualizar(): void;\n}\n\nexport interface AcaoDaPeca {\n  rotulo(): string;\n  rodar(): void;\n  /** Falso = o botão aparece desabilitado, com o rótulo explicando por quê. */\n  ativa?(): boolean;\n}\n\nexport interface Controle {\n  acoes?: Record<number, AcaoDaPeca>;\n  irParaPasso?(i: number): void;\n  tocar(): void;\n  pausar(): void;\n  reiniciar(): void;\n  destruir(): void;\n}\n\nexport type Controlador = (raiz: ParentNode, modelo: Record<string, unknown>, api: AnatomiaApi) => Controle;\n\nexport interface FiguraDef {\n  dados: DadosDaFigura;\n  controlador: Controlador;\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/visant/lib/anatomia-kit.ts",
      "content": "// GERADO por scripts/anatomia-build.mjs a partir de anatomia/kit/iso-kit.ts. Não edite: rode o script.\n// Kit isométrico da skill anatomy (github.com/wheresryan22/anatomy, MIT). Licença em anatomia/kit/LICENSE.\n// @ts-nocheck\nexport type Flat = [number, number];\nexport type Point3 = [number, number, number];\nexport type Plane = [number, number];\n\nexport interface Projection {\n  origin: Flat;\n  scale: number;\n  azimuth?: number;\n  elevation?: number;\n}\n\nexport interface Plan {\n  x: number;\n  y: number;\n  w: number;\n  d: number;\n  r: number;\n}\n\nexport interface SolidPaths {\n  fill: string;\n  outline: string;\n  crease: string;\n  top: string;\n  shades: string[];\n  bevel?: string;\n  seamless?: boolean;\n}\n\nexport type Tone = \"hi\" | \"mid\" | \"lo\" | \"faint\";\nexport type LineTone = Tone | \"lit\" | \"red\" | \"green\" | \"blue\";\n\nexport const AZIMUTH = 45;\nexport const ELEVATION = 30;\nexport const SHADES = 4;\n\nconst RADIANS = Math.PI / 180;\nconst UNIT = Math.sqrt(1.6);\n\ninterface Camera {\n  sinA: number;\n  cosA: number;\n  sinE: number;\n  cosE: number;\n  k: number;\n}\n\nexport function cameraOf({ scale, azimuth = AZIMUTH, elevation = ELEVATION }: Projection): Camera {\n  const a = azimuth * RADIANS;\n  const e = elevation * RADIANS;\n  return { sinA: Math.sin(a), cosA: Math.cos(a), sinE: Math.sin(e), cosE: Math.cos(e), k: scale * UNIT };\n}\n\nexport function iso([x, y, z]: Point3, projection: Projection): Flat {\n  const { origin } = projection;\n  const { sinA, cosA, sinE, cosE, k } = cameraOf(projection);\n  return [origin[0] + (x * sinA - y * cosA) * k, origin[1] + ((x * cosA + y * sinA) * sinE - z * cosE) * k];\n}\n\nexport function towardViewer(projection: Projection): Plane {\n  const { sinA, cosA } = cameraOf(projection);\n  return [cosA, sinA];\n}\n\nexport function depthOf([x, y, z]: Point3, projection: Projection) {\n  const { sinA, cosA, sinE, cosE } = cameraOf(projection);\n  return (x * cosA + y * sinA) * cosE + z * sinE;\n}\n\nconst tenth = (value: number) => {\n  const text = (Math.round(value * 10) / 10).toFixed(1).replace(/\\.0$/, \"\");\n  return text === \"-0\" ? \"0\" : text;\n};\n\nexport function pathOf(points: Flat[], closed = false) {\n  if (!points.length) return \"\";\n  return points.map(([x, y], at) => `${at ? \"L\" : \"M\"}${tenth(x)} ${tenth(y)}`).join(\"\") + (closed ? \"Z\" : \"\");\n}\n\nexport const joinPaths = (paths: string[]) => paths.filter(Boolean).join(\"\");\n\nexport function roundedPlan({ x, y, w, d, r }: Plan, steps = 8): Plane[] {\n  const radius = Math.max(0, Math.min(r, w / 2, d / 2));\n  const corners: [number, number, number][] = [\n    [x + w - radius, y + radius, -90],\n    [x + w - radius, y + d - radius, 0],\n    [x + radius, y + d - radius, 90],\n    [x + radius, y + radius, 180],\n  ];\n  const out: Plane[] = [];\n  for (const [cx, cy, start] of corners) {\n    const count = radius > 0 ? steps : 0;\n    for (let step = 0; step <= count; step++) {\n      const angle = (start + (count ? (90 * step) / count : 45)) * RADIANS;\n      out.push([cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)]);\n    }\n  }\n  return out;\n}\n\nfunction signedArea(points: Plane[] | Flat[]) {\n  let area = 0;\n  for (let index = 0; index < points.length; index++) {\n    const [ax, ay] = points[index]!;\n    const [bx, by] = points[(index + 1) % points.length]!;\n    area += ax * by - bx * ay;\n  }\n  return area / 2;\n}\n\nconst oriented = (points: Flat[]) => (signedArea(points) < 0 ? [...points].reverse() : points);\n\nfunction convexHull(points: Flat[]): Flat[] {\n  const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);\n  if (sorted.length < 3) return sorted;\n  const cross = (o: Flat, a: Flat, b: Flat) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);\n  const lower: Flat[] = [];\n  for (const point of sorted) {\n    while (lower.length >= 2 && cross(lower[lower.length - 2]!, lower[lower.length - 1]!, point) <= 0) lower.pop();\n    lower.push(point);\n  }\n  const upper: Flat[] = [];\n  for (let index = sorted.length - 1; index >= 0; index--) {\n    const point = sorted[index]!;\n    while (upper.length >= 2 && cross(upper[upper.length - 2]!, upper[upper.length - 1]!, point) <= 0) upper.pop();\n    upper.push(point);\n  }\n  return [...lower.slice(0, -1), ...upper.slice(0, -1)];\n}\n\nfunction runsOf(flags: boolean[], want: boolean) {\n  const count = flags.length;\n  const runs: number[][] = [];\n  const start = flags.findIndex((flag, index) => flag !== flags[(index + count - 1) % count]);\n  if (start < 0) return flags[0] === want ? [Array.from({ length: count + 1 }, (_, index) => index % count)] : [];\n  let run: number[] | null = null;\n  for (let step = 0; step < count; step++) {\n    const index = (start + step) % count;\n    if (flags[index] === want) {\n      if (!run) run = [index];\n      run.push((index + 1) % count);\n    } else if (run) {\n      runs.push(run);\n      run = null;\n    }\n  }\n  if (run) runs.push(run);\n  return runs;\n}\n\nexport interface ExtrudeOptions {\n  convex?: boolean;\n  bevel?: number;\n}\n\nexport function extrude(ring: Plane[], z: number, height: number, projection: Projection, options: ExtrudeOptions = {}): SolidPaths {\n  const convex = options.convex ?? true;\n  const count = ring.length;\n  const counter = signedArea(ring) > 0;\n  const [vx, vy] = towardViewer(projection);\n  const { sinA, cosA } = cameraOf(projection);\n  const normals = ring.map(([ax, ay], index) => {\n    const [bx, by] = ring[(index + 1) % count]!;\n    const dx = bx - ax;\n    const dy = by - ay;\n    const length = Math.hypot(dx, dy) || 1;\n    return (counter ? [dy / length, -dx / length] : [-dy / length, dx / length]) as Plane;\n  });\n  const facing = normals.map(([nx, ny]) => nx * vx + ny * vy > 1e-9);\n  const top = ring.map(([x, y]) => iso([x, y, z + height], projection));\n  const bottom = ring.map(([x, y]) => iso([x, y, z], projection));\n  const outline: string[] = [];\n  const crease: string[] = [];\n  for (const run of runsOf(facing, false)) outline.push(pathOf(run.map((index) => top[index]!)));\n  for (const run of runsOf(facing, true)) {\n    crease.push(pathOf(run.map((index) => top[index]!)));\n    if (height > 0) outline.push(pathOf(run.map((index) => bottom[index]!)));\n  }\n  if (height > 0) {\n    for (let index = 0; index < count; index++) {\n      if (facing[(index + count - 1) % count] !== facing[index]) outline.push(pathOf([top[index]!, bottom[index]!]));\n    }\n  }\n  let fill: string;\n  if (convex) {\n    fill = pathOf(convexHull([...top, ...bottom]), true);\n  } else {\n    const parts = [pathOf(oriented(top), true), pathOf(oriented(bottom), true)];\n    for (let index = 0; index < count; index++) {\n      if (!facing[index]) continue;\n      const next = (index + 1) % count;\n      parts.push(pathOf(oriented([top[index]!, top[next]!, bottom[next]!, bottom[index]!]), true));\n    }\n    fill = parts.join(\"\");\n  }\n  const buckets: string[][] = Array.from({ length: SHADES }, () => []);\n  if (height > 0) {\n    const [lx, ly] = [-sinA, cosA];\n    for (let index = 0; index < count; index++) {\n      if (!facing[index]) continue;\n      const [nx, ny] = normals[index]!;\n      const lit = (nx * lx + ny * ly + 1) / 2;\n      const shade = Math.min(SHADES - 1, Math.max(0, Math.floor(lit * SHADES)));\n      const next = (index + 1) % count;\n      buckets[shade]!.push(pathOf(oriented([top[index]!, top[next]!, bottom[next]!, bottom[index]!]), true));\n    }\n  }\n  let bevel: string | undefined;\n  if (options.bevel && options.bevel > 0) {\n    const by = options.bevel;\n    const inset = ring.map(([x, y], index) => {\n      const [ax, ay] = normals[(index + count - 1) % count]!;\n      const [bx, by2] = normals[index]!;\n      const mx = ax + bx;\n      const my = ay + by2;\n      const length = Math.hypot(mx, my) || 1;\n      return [x - (mx / length) * by, y - (my / length) * by] as Plane;\n    });\n    const insetTop = inset.map(([x, y]) => iso([x, y, z + height], projection));\n    bevel = runsOf(facing, true)\n      .map((run) => pathOf([top[run[0]!]!, ...run.slice(1, -1).map((index) => insetTop[index]!), top[run[run.length - 1]!]!]))\n      .join(\"\");\n  }\n  return {\n    fill,\n    outline: outline.join(\"\"),\n    crease: crease.join(\"\"),\n    top: pathOf(oriented(top), true),\n    shades: buckets.map((paths) => paths.join(\"\")),\n    bevel,\n  };\n}\n\nexport const ringOf = (plan: Plan, steps = 8): Plane[] => roundedPlan(plan, steps);\n\nexport function slabOf(plan: Plan, z: number, height: number, projection: Projection, steps = 8, bevel = 0) {\n  return extrude(ringOf(plan, steps), z, height, projection, { convex: true, bevel });\n}\n\nexport function circleRing(cx: number, cy: number, radius: number, steps = 40): Plane[] {\n  return Array.from({ length: steps }, (_, step) => {\n    const angle = (step / steps) * Math.PI * 2;\n    return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)] as Plane;\n  });\n}\n\nexport function cylinder(cx: number, cy: number, radius: number, z: number, height: number, projection: Projection, steps = 32, bevel = 0) {\n  return extrude(circleRing(cx, cy, radius, steps), z, height, projection, { convex: true, bevel });\n}\n\nexport const insetPlan = ({ x, y, w, d, r }: Plan, by: number): Plan => ({ x: x + by, y: y + by, w: w - by * 2, d: d - by * 2, r: Math.max(0, r - by) });\n\nexport function onTop(points: Plane[], z: number, projection: Projection, closed = false) {\n  return pathOf(\n    points.map(([x, y]) => iso([x, y, z], projection)),\n    closed,\n  );\n}\n\nexport const planOutline = (plan: Plan, z: number, projection: Projection, steps = 8) => onTop(ringOf(plan, steps), z, projection, true);\n\nexport const lineOnTop = (from: Plane, to: Plane, z: number, projection: Projection) => onTop([from, to], z, projection);\n\nexport const segment = (from: Point3, to: Point3, projection: Projection) => pathOf([iso(from, projection), iso(to, projection)]);\n\nexport const rise = (at: Plane, from: number, to: number, projection: Projection) => segment([at[0], at[1], from], [at[0], at[1], to], projection);\n\nexport const ring = (cx: number, cy: number, radius: number, z: number, projection: Projection, steps = 40) =>\n  onTop(circleRing(cx, cy, radius, steps), z, projection, true);\n\nexport const pointsAt = (points: Point3[], projection: Projection): Flat[] => points.map((point) => iso(point, projection));\n\nexport function dotGrid(x: number, y: number, columns: number, rows: number, gap: number, z: number, projection: Projection): Flat[] {\n  const dots: Flat[] = [];\n  for (let row = 0; row < rows; row++) {\n    for (let column = 0; column < columns; column++) dots.push(iso([x + column * gap, y + row * gap, z], projection));\n  }\n  return dots;\n}\n\nexport function corners({ x, y, w, d }: Plan, inset = 0): Plane[] {\n  return [\n    [x + inset, y + inset],\n    [x + w - inset, y + inset],\n    [x + inset, y + d - inset],\n    [x + w - inset, y + d - inset],\n  ];\n}\n\nexport function boxCorners({ x, y, w, d }: Plan, z0: number, z1: number): Point3[] {\n  const out: Point3[] = [];\n  for (const px of [x, x + w]) for (const py of [y, y + d]) for (const z of [z0, z1]) out.push([px, py, z]);\n  return out;\n}\n\nexport function knurl(cx: number, cy: number, radius: number, z0: number, z1: number, count: number, projection: Projection, inset = 0.4) {\n  const [vx, vy] = towardViewer(projection);\n  const lines: string[] = [];\n  for (let index = 0; index < count; index++) {\n    const angle = (index / count) * Math.PI * 2;\n    const nx = Math.cos(angle);\n    const ny = Math.sin(angle);\n    if (nx * vx + ny * vy < 0.15) continue;\n    lines.push(segment([cx + radius * nx, cy + radius * ny, z0 + inset], [cx + radius * nx, cy + radius * ny, z1 - inset], projection));\n  }\n  return lines.join(\"\");\n}\n\nexport function sideArc(cx: number, cy: number, radius: number, z: number, projection: Projection, steps = 24) {\n  const [vx, vy] = towardViewer(projection);\n  const facing = Math.atan2(vy, vx);\n  const points: Flat[] = [];\n  for (let step = 0; step <= steps; step++) {\n    const angle = facing - Math.PI / 2 + (step / steps) * Math.PI;\n    points.push(iso([cx + radius * Math.cos(angle), cy + radius * Math.sin(angle), z], projection));\n  }\n  return pathOf(points);\n}\n\nexport function ringSeam(ring: Plane[], z: number, projection: Projection) {\n  const count = ring.length;\n  const counter = signedArea(ring) > 0;\n  const [vx, vy] = towardViewer(projection);\n  const facing = ring.map(([ax, ay], index) => {\n    const [bx, by] = ring[(index + 1) % count];\n    const dx = bx - ax;\n    const dy = by - ay;\n    const [nx, ny] = counter ? [dy, -dx] : [-dy, dx];\n    return nx * vx + ny * vy > 1e-9;\n  });\n  return runsOf(facing, true).map((run) => pathOf(run.map((index) => iso([ring[index][0], ring[index][1], z], projection)))).join(\"\");\n}\n\nexport const sideSeam = (plan: Plan, z: number, projection: Projection, steps = 8) => ringSeam(ringOf(plan, steps), z, projection);\n\nexport function sideRing(center: Point3, radius: number, axis: \"x\" | \"y\", projection: Projection, steps = 20) {\n  const [px, py, pz] = center;\n  const points: Flat[] = [];\n  for (let step = 0; step < steps; step++) {\n    const angle = (step / steps) * Math.PI * 2;\n    const u = radius * Math.cos(angle);\n    const v = radius * Math.sin(angle);\n    points.push(iso(axis === \"x\" ? [px, py + u, pz + v] : [px + u, py, pz + v], projection));\n  }\n  return pathOf(points, true);\n}\n\nexport function coil(from: number, to: number, center: Plane, radius: number, turns: number, axis: \"x\" | \"y\", projection: Projection, perTurn = 16) {\n  const count = Math.max(1, Math.round(turns * perTurn));\n  const back: string[] = [];\n  const front: string[] = [];\n  let run: Flat[] = [];\n  let near: boolean | null = null;\n  for (let index = 0; index <= count; index++) {\n    const share = index / count;\n    const angle = share * turns * Math.PI * 2;\n    const u = radius * Math.cos(angle);\n    const v = radius * Math.sin(angle);\n    const along = from + (to - from) * share;\n    const point: Point3 = axis === \"x\" ? [along, center[0] + u, center[1] + v] : [center[0] + u, along, center[1] + v];\n    const normal: Point3 = axis === \"x\" ? [0, Math.cos(angle), Math.sin(angle)] : [Math.cos(angle), 0, Math.sin(angle)];\n    const facing = depthOf(normal, projection) > 0;\n    const flat = iso(point, projection);\n    if (near === null) near = facing;\n    if (facing !== near) {\n      run.push(flat);\n      (near ? front : back).push(pathOf(run));\n      run = [flat];\n      near = facing;\n      continue;\n    }\n    run.push(flat);\n  }\n  if (run.length > 1) (near ? front : back).push(pathOf(run));\n  return { back: back.join(\"\"), front: front.join(\"\") };\n}\n\nexport interface Ticks {\n  minor: string;\n  major: string;\n}\n\nexport function topTicks(from: number, to: number, step: number, every: number, edge: number, z: number, lengths: [number, number], projection: Projection, axis: \"x\" | \"y\" = \"x\"): Ticks {\n  const minor: string[] = [];\n  const major: string[] = [];\n  const first = Math.ceil(from / step) * step;\n  for (let at = first, index = Math.round(first / step); at <= to + 1e-9; at += step, index++) {\n    const big = index % every === 0;\n    const length = big ? lengths[1] : lengths[0];\n    const a: Plane = axis === \"x\" ? [at, edge] : [edge, at];\n    const b: Plane = axis === \"x\" ? [at, edge + length] : [edge + length, at];\n    (big ? major : minor).push(lineOnTop(a, b, z, projection));\n  }\n  return { minor: minor.join(\"\"), major: major.join(\"\") };\n}\n\nexport function sideTicks(from: number, to: number, step: number, every: number, face: number, top: number, lengths: [number, number], projection: Projection, axis: \"x\" | \"y\" = \"x\"): Ticks {\n  const minor: string[] = [];\n  const major: string[] = [];\n  const first = Math.ceil(from / step) * step;\n  for (let at = first, index = Math.round(first / step); at <= to + 1e-9; at += step, index++) {\n    const big = index % every === 0;\n    const length = big ? lengths[1] : lengths[0];\n    const a: Point3 = axis === \"x\" ? [at, face, top] : [face, at, top];\n    const b: Point3 = axis === \"x\" ? [at, face, top - length] : [face, at, top - length];\n    (big ? major : minor).push(segment(a, b, projection));\n  }\n  return { minor: minor.join(\"\"), major: major.join(\"\") };\n}\n\nexport function radialTicks(cx: number, cy: number, radius: number, count: number, every: number, z: number, lengths: [number, number], projection: Projection, from = 0, span = Math.PI * 2): Ticks {\n  const minor: string[] = [];\n  const major: string[] = [];\n  const closed = Math.abs(span - Math.PI * 2) < 1e-9;\n  const steps = closed ? count : count + 1;\n  for (let index = 0; index < steps; index++) {\n    const angle = from + (index / count) * span;\n    const big = index % every === 0;\n    const inner = radius - (big ? lengths[1] : lengths[0]);\n    const sx = Math.sin(angle);\n    const sy = -Math.cos(angle);\n    (big ? major : minor).push(lineOnTop([cx + sx * inner, cy + sy * inner], [cx + sx * radius, cy + sy * radius], z, projection));\n  }\n  return { minor: minor.join(\"\"), major: major.join(\"\") };\n}\n\nexport const haloOf = (plan: Plan, z: number, height: number, projection: Projection) => slabOf(plan, z, height, projection).fill;\n\nexport function axisVector(projection: Projection, [dx, dy, dz]: Point3): Flat {\n  const origin = iso([0, 0, 0], projection);\n  const moved = iso([dx, dy, dz], projection);\n  return [moved[0] - origin[0], moved[1] - origin[1]];\n}\n\nconst fine = (value: number) => Math.round(value * 100) / 100;\n\nexport function translateAlong(projection: Projection, by: Point3) {\n  const [x, y] = axisVector(projection, by);\n  return `translate(${fine(x)} ${fine(y)})`;\n}\n\nconst thousandth = (value: number) => Math.round(value * 1000) / 1000;\n\nfunction axesAt(at: Plane, z: number, projection: Projection) {\n  const origin = iso([at[0], at[1], z], projection);\n  const along = (point: Point3): Flat => {\n    const [px, py] = iso(point, projection);\n    return [px - origin[0], py - origin[1]];\n  };\n  return { origin, x: along([at[0] + 1, at[1], z]), y: along([at[0], at[1] + 1, z]), z: along([at[0], at[1], z + 1]) };\n}\n\nconst matrixOf = (u: Flat, v: Flat, origin: Flat) =>\n  `matrix(${thousandth(u[0])} ${thousandth(u[1])} ${thousandth(v[0])} ${thousandth(v[1])} ${thousandth(origin[0])} ${thousandth(origin[1])})`;\n\nexport function holdStill(element: Element, [dx, dy]: Flat) {\n  const text = dx || dy ? `translate(${-dx} ${-dy})` : \"\";\n  element.querySelectorAll(\"[data-hold]\").forEach((node) => (text ? node.setAttribute(\"transform\", text) : node.removeAttribute(\"transform\")));\n}\n\nexport function topMatrix(at: Plane, z: number, projection: Projection, along: \"x\" | \"y\" = \"x\") {\n  const { origin, x, y } = axesAt(at, z, projection);\n  return along === \"x\" ? matrixOf(x, y, origin) : matrixOf([-y[0], -y[1]], x, origin);\n}\n\nexport function sideMatrix(at: Plane, z: number, projection: Projection, face: \"left\" | \"right\") {\n  const { origin, x, y, z: up } = axesAt(at, z, projection);\n  const down: Flat = [-up[0], -up[1]];\n  return face === \"left\" ? matrixOf(x, down, origin) : matrixOf([-y[0], -y[1]], down, origin);\n}\n\nexport function screenRise(height: number, projection: Projection) {\n  const { cosE, k } = cameraOf(projection);\n  return height * cosE * k;\n}\n\nexport interface FitOptions {\n  pad?: number;\n  headroom?: number;\n  azimuth?: number;\n  elevation?: number;\n}\n\nexport function fitProjection(points: Point3[], width: number, height: number, { pad = 24, headroom = 0, azimuth = AZIMUTH, elevation = ELEVATION }: FitOptions = {}): Projection {\n  const unit: Projection = { origin: [0, 0], scale: 1, azimuth, elevation };\n  const flat = points.map((point) => iso(point, unit));\n  const xs = flat.map(([x]) => x);\n  const ys = flat.map(([, y]) => y);\n  const left = Math.min(...xs);\n  const right = Math.max(...xs);\n  const up = Math.min(...ys);\n  const down = Math.max(...ys);\n  const scale = Math.min((width - pad * 2) / (right - left), (height - pad * 2 - headroom) / (down - up));\n  return {\n    origin: [width / 2 - ((left + right) / 2) * scale, headroom + (height - headroom) / 2 - ((up + down) / 2) * scale],\n    scale,\n    azimuth,\n    elevation,\n  };\n}\n\nconst escape = (text: string) => text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n\nconst attrs = (pairs: Record<string, string | number | undefined | false>) =>\n  Object.entries(pairs)\n    .filter(([, value]) => value !== undefined && value !== false)\n    .map(([key, value]) => ` ${key}=\"${escape(String(value))}\"`)\n    .join(\"\");\n\nexport interface SolidStyle {\n  tone?: Tone;\n  crease?: Tone | \"none\";\n  halo?: boolean;\n  lit?: boolean;\n  flat?: boolean;\n  className?: string;\n  inner?: string;\n}\n\nexport function solidSvg(paths: SolidPaths, { tone = \"hi\", crease = \"faint\", halo = false, lit = false, flat = false, className, inner = \"\" }: SolidStyle = {}) {\n  const parts: string[] = [];\n  if (halo) parts.push(`<path class=\"iso-halo\" d=\"${paths.fill}\"/>`);\n  parts.push(`<path class=\"iso-fill\" d=\"${paths.fill}\"/>`);\n  if (!flat) {\n    paths.shades.forEach((d, shade) => {\n      if (d) parts.push(`<path class=\"iso-shade\" data-shade=\"${shade}\" d=\"${d}\"/>`);\n    });\n    parts.push(`<path class=\"iso-top\" d=\"${paths.top}\"/>`);\n  }\n  if (crease !== \"none\" && paths.crease) parts.push(`<path class=\"iso-line iso-crease\" data-tone=\"${crease}\" d=\"${paths.crease}\"/>`);\n  if (crease !== \"none\" && paths.bevel) parts.push(`<path class=\"iso-line iso-bevel\" data-tone=\"${crease}\" d=\"${paths.bevel}\"/>`);\n  if (inner) parts.push(inner);\n  parts.push(`<path class=\"iso-line iso-edge\" data-tone=\"${tone}\" d=\"${paths.outline}\"/>`);\n  return `<g${attrs({ class: className ? `iso-solid ${className}` : \"iso-solid\", \"data-lit\": lit ? \"\" : undefined, \"data-seamless\": paths.seamless ? \"\" : undefined })}>${parts.join(\"\")}</g>`;\n}\n\nexport interface LineStyle {\n  tone?: LineTone;\n  dotted?: boolean;\n  dashed?: boolean;\n  flow?: boolean;\n  className?: string;\n  free?: boolean | \"start\" | \"end\";\n}\n\nexport function lineSvg(d: string, { tone = \"lo\", dotted = false, dashed = false, flow = false, className, free }: LineStyle = {}) {\n  if (!d) return \"\";\n  return `<path${attrs({\n    class: className ? `iso-line ${className}` : \"iso-line\",\n    \"data-tone\": tone,\n    \"data-free\": free === true ? \"\" : free || undefined,\n    \"data-dotted\": dotted ? \"\" : undefined,\n    \"data-dashed\": dashed ? \"\" : undefined,\n    \"data-flow\": flow ? \"\" : undefined,\n    d,\n  })}/>`;\n}\n\nexport function dotsSvg(points: Array<Flat | [number, number, number]>, { size = 0.5, tone = \"mid\", pulse = false }: { size?: number; tone?: Tone; pulse?: boolean } = {}) {\n  const dots = points\n    .map(([x, y, alpha]: number[], index) => `<circle cx=\"${tenth(x)}\" cy=\"${tenth(y)}\" r=\"${size}\"${alpha !== undefined && alpha < 1 ? ` fill-opacity=\"${Math.max(0, alpha).toFixed(2)}\"` : \"\"}${pulse ? ` data-pulse=\"\" style=\"--pulse-at:${(index * 137) % 1600}ms\"` : \"\"}/>`)\n    .join(\"\");\n  return `<g class=\"iso-dots\" data-tone=\"${tone}\">${dots}</g>`;\n}\n\nexport type Faded = { d: string; alpha: number } | [string, number];\n\nexport function fadedSvg(list: Faded[], style: LineStyle = {}) {\n  const groups = new Map<number, string>();\n  for (const entry of list) {\n    const [d, alpha] = Array.isArray(entry) ? entry : [entry.d, entry.alpha];\n    if (!d || !(alpha > 0.02)) continue;\n    const level = Math.min(1, Math.round(alpha * 20) / 20);\n    groups.set(level, (groups.get(level) ?? \"\") + d);\n  }\n  return [...groups].map(([alpha, d]) => (alpha >= 1 ? lineSvg(d, style) : `<g opacity=\"${alpha.toFixed(2)}\">${lineSvg(d, style)}</g>`)).join(\"\");\n}\n\nexport interface FadeLineStyle {\n  fade?: number | [number, number];\n  tone?: LineTone;\n  className?: string;\n  stops?: number;\n}\n\nexport function fadeLineSvg(points: Flat[], { fade = [0, 0], tone = \"lo\", className, stops = 6 }: FadeLineStyle = {}) {\n  if (points.length < 2) return \"\";\n  const ease = (t: number) => t * t * (3 - 2 * t);\n  const hashOf = (text: string) => {\n    let hash = 2166136261;\n    for (let index = 0; index < text.length; index++) hash = Math.imul(hash ^ text.charCodeAt(index), 16777619);\n    return (hash >>> 0).toString(36);\n  };\n  const lengths = [0];\n  for (let index = 1; index < points.length; index++) lengths.push(lengths[index - 1] + Math.hypot(points[index][0] - points[index - 1][0], points[index][1] - points[index - 1][1]));\n  const at = (length: number): Flat => {\n    let index = 1;\n    while (index < points.length - 1 && lengths[index] < length) index++;\n    const span = lengths[index] - lengths[index - 1] || 1;\n    const t = Math.max(0, Math.min(1, (length - lengths[index - 1]) / span));\n    return [points[index - 1][0] + (points[index][0] - points[index - 1][0]) * t, points[index - 1][1] + (points[index][1] - points[index - 1][1]) * t];\n  };\n  const pieceOf = (from: number, to: number): Flat[] => [at(from), ...points.filter((_, index) => lengths[index] > from + 1e-6 && lengths[index] < to - 1e-6), at(to)];\n  const total = lengths[lengths.length - 1];\n  if (total < 1e-6) return \"\";\n  let [start, end] = Array.isArray(fade) ? fade : [fade, fade];\n  if (start + end > total) {\n    const share = total / (start + end);\n    start *= share;\n    end *= share;\n  }\n  const paint = `var(--anatomy-${tone})`;\n  const ramp = (piece: Flat[], rising: boolean) => {\n    const d = pathOf(piece);\n    const [x1, y1] = piece[0];\n    const [x2, y2] = piece[piece.length - 1];\n    const id = `iso-fade-${hashOf(`${d}|${tone}|${rising}`)}`;\n    const marks = Array.from({ length: stops + 1 }, (_, index) => {\n      const t = index / stops;\n      const alpha = ease(rising ? t : 1 - t);\n      return `<stop offset=\"${t.toFixed(3)}\" stop-opacity=\"${alpha.toFixed(3)}\" style=\"stop-color:${paint}\"/>`;\n    }).join(\"\");\n    const gradient = `<linearGradient id=\"${id}\" gradientUnits=\"userSpaceOnUse\" x1=\"${tenth(x1)}\" y1=\"${tenth(y1)}\" x2=\"${tenth(x2)}\" y2=\"${tenth(y2)}\">${marks}</linearGradient>`;\n    return `<defs>${gradient}</defs><path${attrs({ class: className ? `iso-line ${className}` : \"iso-line\", \"data-tone\": tone, \"data-free\": rising ? \"start\" : \"end\", d, style: `stroke:url(#${id})` })}/>`;\n  };\n  const parts: string[] = [];\n  if (start > 0.05) parts.push(ramp(pieceOf(0, start), true));\n  if (total - end - start > 0.05) parts.push(lineSvg(pathOf(pieceOf(start, total - end)), { tone, className }));\n  if (end > 0.05) parts.push(ramp(pieceOf(total - end, total), false));\n  return `<g class=\"iso-fade\">${parts.join(\"\")}</g>`;\n}\n\nexport interface WireStyle {\n  via?: Flat[];\n  tone?: LineTone;\n  dotted?: boolean;\n  dashed?: boolean;\n  flow?: boolean;\n  ends?: \"both\" | \"from\" | \"to\" | \"none\";\n  size?: number;\n}\n\nexport function wireSvg(from: Flat, to: Flat, { via = [], tone = \"lo\", dotted = false, dashed = false, flow = false, ends = \"both\", size = 1.2 }: WireStyle = {}) {\n  const d = pathOf([from, ...via, to]);\n  const end = ([x, y]: Flat) => `<circle class=\"iso-wire-end\" cx=\"${tenth(x)}\" cy=\"${tenth(y)}\" r=\"${size}\"/>`;\n  return `<g class=\"iso-wire\" data-tone=\"${tone}\">${lineSvg(d, { tone, dotted, dashed, flow })}${ends === \"both\" || ends === \"from\" ? end(from) : \"\"}${ends === \"both\" || ends === \"to\" ? end(to) : \"\"}</g>`;\n}\n\nexport function faceTextSvg(transform: string, text: string, { size = 3, tone = \"mid\", anchor = \"start\" }: { size?: number; tone?: Tone; anchor?: \"start\" | \"middle\" | \"end\" } = {}) {\n  return `<text class=\"iso-face-text\" data-tone=\"${tone}\" transform=\"${transform}\" font-size=\"${size}\" text-anchor=\"${anchor}\">${escape(text)}</text>`;\n}\n\nexport function groupSvg(children: string | string[], extra: Record<string, string | number | undefined | false> = {}) {\n  return `<g${attrs(extra)}>${Array.isArray(children) ? children.join(\"\") : children}</g>`;\n}\n\nexport type Theme = \"dark\" | \"light\";\n\nexport interface FigureOptions {\n  width: number;\n  height: number;\n  label: string;\n  body: string | string[];\n  standalone?: Theme | false;\n  className?: string;\n}\n\nexport function figureSvg({ width, height, label, body, standalone = false, className }: FigureOptions) {\n  const style = \"\";\n  return `<svg${attrs({\n    xmlns: \"http://www.w3.org/2000/svg\",\n    class: `iso-svg${standalone ? \" iso\" : \"\"}${className ? ` ${className}` : \"\"}`,\n    \"data-theme\": standalone || undefined,\n    viewBox: `0 0 ${width} ${height}`,\n    role: \"img\",\n    \"aria-label\": label,\n  })}>${style}${Array.isArray(body) ? body.join(\"\") : body}</svg>`;\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/visant/lib/anatomia-estilo.ts",
      "content": "// GERADO por scripts/anatomia-build.mjs a partir de anatomia/kit/iso-kit.mjs (ISO_CSS, só as regras). Não edite: rode o script.\n/** Regras do kit com os tons vindos dos tokens do site (--foreground, --card, --brand). */\nexport const ESTILO_DO_KIT = \".va-palco{--anatomy-card:var(--va-bg);--anatomy-paper:var(--va-bg);--anatomy-text:var(--va-muted);--anatomy-strong:var(--va-fg);--anatomy-muted:color-mix(in oklab, var(--va-fg) 45%, var(--va-bg));--anatomy-face:color-mix(in oklab, white 4%, var(--va-bg));--anatomy-top:color-mix(in oklab, white 5%, var(--va-bg));--anatomy-shade-0:color-mix(in oklab, black 16%, var(--va-bg));--anatomy-shade-1:color-mix(in oklab, black 11%, var(--va-bg));--anatomy-shade-2:color-mix(in oklab, black 6.5%, var(--va-bg));--anatomy-shade-3:color-mix(in oklab, black 2.5%, var(--va-bg));--anatomy-lit-top:color-mix(in oklab, white 11%, var(--va-bg));--anatomy-lit-shade:color-mix(in oklab, white 6%, var(--va-bg));--anatomy-hi:color-mix(in oklab, var(--va-fg) 84%, var(--va-bg));--anatomy-mid:color-mix(in oklab, var(--va-fg) 46%, var(--va-bg));--anatomy-lo:color-mix(in oklab, var(--va-fg) 26%, var(--va-bg));--anatomy-faint:color-mix(in oklab, var(--va-fg) 14%, var(--va-bg));--anatomy-lit:var(--va-brand);--anatomy-halo:color-mix(in oklab, black 70%, var(--va-bg));--anatomy-halo-opacity:.55;--anatomy-dot:var(--va-fg);--anatomy-red:var(--destructive, crimson);--anatomy-green:var(--success, seagreen);--anatomy-blue:var(--info, royalblue);--anatomy-weight:.6px;--anatomy-ease:var(--ease-out, cubic-bezier(.32,.72,0,1));--anatomy-mono:var(--va-mono)}\\n.iso-svg{display:block;width:100%;height:auto;overflow:visible}\\n.iso-svg path,.iso-svg circle{vector-effect:non-scaling-stroke}\\n.iso-fill{fill:var(--anatomy-paper);stroke:none}\\n.iso-solid[data-lit]>.iso-fill{fill:var(--anatomy-face)}\\n.iso-shade,.iso-top{stroke:none}\\n.iso-shade[data-shade=\\\"0\\\"]{fill:var(--anatomy-shade-0)}\\n.iso-shade[data-shade=\\\"1\\\"]{fill:var(--anatomy-shade-1)}\\n.iso-shade[data-shade=\\\"2\\\"]{fill:var(--anatomy-shade-2)}\\n.iso-shade[data-shade=\\\"3\\\"]{fill:var(--anatomy-shade-3)}\\n.iso-top{fill:var(--anatomy-top)}\\n.iso-solid[data-lit]>.iso-top{fill:var(--anatomy-lit-top)}\\n.iso-solid[data-lit]>.iso-shade{fill:var(--anatomy-lit-shade)}\\n.iso-solid[data-seamless]>.iso-shade,.iso-solid[data-seamless]>.iso-top{stroke-width:.4;stroke-linejoin:round}\\n.iso-solid[data-seamless]>.iso-shade[data-shade=\\\"0\\\"]{stroke:var(--anatomy-shade-0)}\\n.iso-solid[data-seamless]>.iso-shade[data-shade=\\\"1\\\"]{stroke:var(--anatomy-shade-1)}\\n.iso-solid[data-seamless]>.iso-shade[data-shade=\\\"2\\\"]{stroke:var(--anatomy-shade-2)}\\n.iso-solid[data-seamless]>.iso-shade[data-shade=\\\"3\\\"]{stroke:var(--anatomy-shade-3)}\\n.iso-solid[data-seamless]>.iso-top{stroke:var(--anatomy-top)}\\n.iso-solid[data-seamless][data-lit]>.iso-top{stroke:var(--anatomy-lit-top)}\\n.iso-solid[data-seamless][data-lit]>.iso-shade{stroke:var(--anatomy-lit-shade)}\\n.iso-solid[data-lit]>.iso-edge{stroke:var(--anatomy-lit)}\\n.iso-halo{fill:var(--anatomy-halo);stroke:none;filter:blur(9px);opacity:var(--anatomy-halo-opacity);transform:translateY(3px)}\\n.iso-line{fill:none;stroke:var(--anatomy-lo);stroke-width:var(--anatomy-weight);stroke-linecap:round;stroke-linejoin:round}\\n.iso-line[data-tone=\\\"hi\\\"]{stroke:var(--anatomy-hi)}\\n.iso-line[data-tone=\\\"mid\\\"]{stroke:var(--anatomy-mid)}\\n.iso-line[data-tone=\\\"lo\\\"]{stroke:var(--anatomy-lo)}\\n.iso-line[data-tone=\\\"faint\\\"]{stroke:var(--anatomy-faint)}\\n.iso-line[data-tone=\\\"lit\\\"]{stroke:var(--anatomy-lit)}\\n.iso-line[data-tone=\\\"red\\\"]{stroke:var(--anatomy-red)}\\n.iso-line[data-tone=\\\"green\\\"]{stroke:var(--anatomy-green)}\\n.iso-line[data-tone=\\\"blue\\\"]{stroke:var(--anatomy-blue)}\\n.iso-line[data-dotted]{stroke-dasharray:.01 3.5;stroke-width:1}\\n.iso-line[data-dashed]{stroke-dasharray:3 3}\\n.iso-line[data-flow]{stroke-dasharray:2 6}\\n.iso-wire-end{fill:var(--anatomy-paper);stroke:var(--anatomy-lo);stroke-width:var(--anatomy-weight)}\\n.iso-wire[data-tone=\\\"hi\\\"] .iso-wire-end{stroke:var(--anatomy-hi)}\\n.iso-wire[data-tone=\\\"mid\\\"] .iso-wire-end{stroke:var(--anatomy-mid)}\\n.iso-wire[data-tone=\\\"faint\\\"] .iso-wire-end{stroke:var(--anatomy-faint)}\\n.iso-wire[data-tone=\\\"lit\\\"] .iso-wire-end{stroke:var(--anatomy-lit)}\\n.iso-dots circle{fill:var(--anatomy-dot);stroke:none;opacity:.32}\\n.iso-dots[data-tone=\\\"hi\\\"] circle{opacity:.85}\\n.iso-dots[data-tone=\\\"mid\\\"] circle{opacity:.45}\\n.iso-dots[data-tone=\\\"lo\\\"] circle{opacity:.22}\\n.iso-dots[data-tone=\\\"faint\\\"] circle{opacity:.12}\\n.iso-face-text{fill:var(--anatomy-mid);font-family:var(--anatomy-mono);letter-spacing:.04em}\\n.iso-face-text[data-tone=\\\"hi\\\"]{fill:var(--anatomy-hi)}\\n.iso-face-text[data-tone=\\\"lo\\\"]{fill:var(--anatomy-lo)}\\n.iso-face-text[data-tone=\\\"faint\\\"]{fill:var(--anatomy-faint)}\\n.tb-body{fill:var(--anatomy-shade-2);stroke:none}\\n.tb-shine{fill:var(--anatomy-top);stroke:none}\\n.tb-shade{fill:var(--anatomy-shade-0);stroke:none}\\n.tb-ring{fill:none;stroke:var(--anatomy-lo);stroke-width:.45;stroke-linecap:butt;stroke-linejoin:round}\\n.tb-ring[data-tone=\\\"mid\\\"]{stroke:var(--anatomy-mid)}\\n.tb-ring[data-tone=\\\"faint\\\"]{stroke:var(--anatomy-faint)}\\n.tb-ring[data-tone=\\\"hi\\\"]{stroke:var(--anatomy-hi)}\\n@keyframes iso-flow{to{stroke-dashoffset:-32}}\\n@keyframes iso-pulse{0%,100%{opacity:.18}12%{opacity:1}40%{opacity:.18}}\\n.iso-line[data-flow]{animation:iso-flow 1600ms linear infinite}\\n.iso-dots circle[data-pulse]{animation:iso-pulse 1600ms steps(1,end) infinite;animation-delay:var(--pulse-at,0ms)}\\n@media (prefers-reduced-motion:reduce){.iso-line[data-flow],.iso-dots circle[data-pulse]{animation:none}}\\n.iso-logo{fill:var(--anatomy-mid);stroke:none}.iso-logo[data-tone=\\\"hi\\\"]{fill:var(--anatomy-hi)}.iso-logo[data-tone=\\\"lo\\\"]{fill:var(--anatomy-lo)}.iso-logo[data-tone=\\\"lit\\\"]{fill:var(--anatomy-lit)}\\n\";\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/visant/lib/anatomia-nucleo.ts",
      "content": "/**\n * Anatomia: figura didática interativa como web component, `<visant-anatomia>`.\n *\n * Um objeto isométrico que explica uma ideia funcionando (skill anatomy, MIT),\n * com a UI de componente em volta: pinos, painel de peças, passo a passo,\n * pausa, recomeço e tela cheia. Roda igual em aula, blog, livro e landing page,\n * porque mora num Shadow DOM e só herda do site as variáveis de tema\n * (--foreground, --card, --brand, --font-sans...). Sem elas, cai nas cores do\n * sistema (Canvas/CanvasText), então nunca fica sem contraste.\n *\n *   registrarFigura(comoSobe); definirAnatomia();\n *   <visant-anatomia figura=\"como-sobe\"></visant-anatomia>\n *\n * Decisões que custaram caro (vieram de teste com aluno e com o dono):\n * - A dica NÃO segue o mouse. A primeira versão abria um balão ao lado do pino\n *   e o mouse atravessava o desenho no caminho até o botão, trocando a peça no\n *   meio. Agora o texto mora num painel fixo e no palco só aparece o nome.\n * - O foco tem ímã e intenção: pino perto ganha da peça embaixo, e só troca de\n *   peça quando o mouse para uns 90 ms (passar rápido por cima não pisca).\n * - O hover nunca mexe em `transform`: peça que se move tem o transform no\n *   grupo de dentro, e um transform de CSS por cima faria ela voltar pro lugar.\n */\nimport * as kit from \"./anatomia-kit\";\nimport { ESTILO_DO_KIT } from \"./anatomia-estilo\";\nimport type { AnatomiaApi, Controle, FiguraDef, Ponto3 } from \"./anatomia-tipos\";\n\nexport type { FiguraDef } from \"./anatomia-tipos\";\n\nconst figuras = new Map<string, FiguraDef>();\n\n/** Deixa a figura disponível pelo atributo `figura=\"<id>\"`. */\nexport function registrarFigura(def: FiguraDef) {\n  figuras.set(def.dados.id, def);\n  document.querySelectorAll<VisantAnatomia>(`visant-anatomia[figura=\"${def.dados.id}\"]`).forEach((el) => el.montar?.());\n}\n\nconst ICONES = {\n  pausar: '<path d=\"M6 4h3v12H6zM11 4h3v12h-3z\"/>',\n  tocar: '<path d=\"M6 4l10 6-10 6z\"/>',\n  reiniciar: '<path d=\"M10 4a6 6 0 1 1-6 6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/><path d=\"M4 3v4h4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>',\n  tela: '<path d=\"M4 8V4h4M16 8V4h-4M4 12v4h4M16 12v4h-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>',\n};\nconst icone = (nome: keyof typeof ICONES) => `<svg viewBox=\"0 0 20 20\" width=\"16\" height=\"16\" aria-hidden=\"true\" fill=\"currentColor\">${ICONES[nome]}</svg>`;\nconst esc = (s: string) => s.replace(/[&<>\"]/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\" })[c]!);\n\nconst CSS = `\n:host{display:block;\n  --va-fg:var(--foreground, CanvasText);--va-bg:var(--card, var(--background, Canvas));\n  --va-muted:var(--muted-foreground, color-mix(in oklab, var(--va-fg) 58%, var(--va-bg)));\n  --va-borda:var(--border, color-mix(in oklab, var(--va-fg) 13%, var(--va-bg)));\n  --va-suave:color-mix(in oklab, var(--va-fg) 5%, var(--va-bg));\n  --va-brand:var(--brand, var(--va-fg));--va-brand-fg:var(--brand-foreground, var(--va-bg));\n  --va-raio:var(--r-surface, var(--radius, 10px));--va-raio-c:var(--r-control, 6px);\n  --va-sans:var(--font-sans, ui-sans-serif, system-ui, sans-serif);--va-mono:var(--font-mono, ui-monospace, \"SF Mono\", Menlo, monospace);\n  --va-ease:var(--ease-out, cubic-bezier(.2,.8,.2,1));\n  color:var(--va-fg);font-family:var(--va-sans);-webkit-font-smoothing:antialiased}\n:host([tema=\"escuro\"]){color-scheme:dark}:host([tema=\"claro\"]){color-scheme:light}\n:host(:fullscreen){background:var(--va-bg);overflow:auto;padding:24px}\n*,*::before,*::after{box-sizing:border-box}\nbutton{font:inherit;color:inherit}\n.va{container-type:inline-size;margin:0;border:1px solid var(--va-borda);border-radius:var(--va-raio);background:var(--va-bg);overflow:hidden}\n.va-topo{display:flex;align-items:center;gap:12px;padding:12px 14px 12px 18px;border-bottom:1px solid var(--va-borda)}\n.va-id{display:flex;align-items:baseline;gap:10px;min-width:0;flex:1}\n.va-fig{font:500 11px/1 var(--va-mono);color:var(--va-muted);letter-spacing:.04em;white-space:nowrap}\n.va-titulo{margin:0;font-size:14px;font-weight:600;letter-spacing:-.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.va-ferramentas{display:flex;align-items:center;gap:4px}\n.va-modos{display:inline-flex;padding:2px;margin-right:6px;border-radius:calc(var(--va-raio-c) + 2px);background:var(--va-suave);border:1px solid var(--va-borda)}\n.va-modos button{border:0;background:transparent;padding:5px 10px;border-radius:var(--va-raio-c);font-size:12px;font-weight:600;color:var(--va-muted);cursor:pointer;transition:background .2s,color .2s}\n.va-modos button[aria-selected=\"true\"]{background:var(--va-bg);color:var(--va-fg);box-shadow:0 0 0 1px var(--va-borda)}\n.va-icone{display:inline-grid;place-items:center;width:30px;height:30px;border:0;border-radius:var(--va-raio-c);background:transparent;color:var(--va-muted);cursor:pointer;transition:background .2s,color .2s}\n.va-icone:hover{background:var(--va-suave);color:var(--va-fg)}\n:is(button,.va-palco):focus-visible{outline:2px solid var(--focus, var(--va-brand));outline-offset:2px}\n.va-corpo{display:grid}\n@container (min-width:760px){.va-corpo{grid-template-columns:minmax(0,1fr) 300px}.va-painel{border-left:1px solid var(--va-borda);border-top:0!important}}\n.va-palco{position:relative;padding:28px 24px 44px;outline:none;min-width:0}\n.va-arte{position:relative;max-width:820px;margin:0 auto;touch-action:manipulation}\n.va-arte svg{display:block;width:100%;height:auto;overflow:visible}\n.va-leitura{position:absolute;left:18px;right:18px;bottom:14px;font:500 11px/1.3 var(--va-mono);color:var(--va-muted);font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.va-pinos{position:absolute;inset:0;pointer-events:none}\n.va-pino{position:absolute;width:22px;height:22px;margin:-11px 0 0 -11px;padding:0;border-radius:50%;border:1px solid color-mix(in oklab, var(--va-fg) 45%, transparent);background:var(--va-bg);color:var(--va-fg);font:600 11px/20px var(--va-mono);text-align:center;cursor:pointer;pointer-events:auto;opacity:0;transform:scale(.6);transition:opacity .22s var(--va-ease),transform .22s var(--va-ease),background .18s,color .18s;box-shadow:0 0 0 4px color-mix(in oklab, var(--va-bg) 70%, transparent)}\n.va[data-interagindo] .va-pino,.va[data-modo=\"passos\"] .va-pino[data-ativa]{opacity:1;transform:none}\n.va[data-focando] .va-pino:not([data-ativa]){opacity:.28}\n.va[data-focando] .va-pino[data-ativa]{opacity:1}\n.va[data-focando] .va-arte svg [data-peca][data-junto]:not([data-ativa]){opacity:.82}\n.va-pino[data-ativa]{background:var(--va-fg);color:var(--va-bg);border-color:var(--va-fg);transform:scale(1.12)}\n.va-pino[hidden]{display:none}\n@media (hover:none){.va .va-pino{opacity:1;transform:none}}\n.va-etiqueta{position:absolute;padding:4px 9px;border-radius:999px;background:var(--va-fg);color:var(--va-bg);font-size:12px;font-weight:600;white-space:nowrap;pointer-events:none;opacity:0;transform:translate(-50%,calc(-100% - 18px)) translateY(4px);transition:opacity .16s,transform .16s var(--va-ease)}\n.va-etiqueta[data-visivel]{opacity:1;transform:translate(-50%,calc(-100% - 18px))}\n.va-arte svg [data-peca]{transition:opacity .28s var(--va-ease),filter .28s var(--va-ease)}\n.va[data-focando] .va-arte svg [data-peca]:not([data-ativa]){opacity:var(--va-apagado,.42)}\n.va-arte svg [data-peca]:not([data-peca=\"0\"]){cursor:pointer}\n.va-painel{border-top:1px solid var(--va-borda);padding:14px;min-width:0;display:flex;flex-direction:column;gap:10px;max-height:min(560px,80vh);overflow:auto}\n.va-rotulo{margin:2px 4px 0;font:500 11px/1 var(--va-mono);color:var(--va-muted);letter-spacing:.04em}\n.va-lista{list-style:none;margin:0;padding:0;display:grid;gap:2px}\n.va-item{display:flex;align-items:center;gap:10px;width:100%;padding:8px;border:0;border-radius:var(--va-raio-c);background:transparent;text-align:left;font-size:13px;font-weight:600;cursor:pointer;transition:background .18s}\n.va-item:hover,.va-item[data-ativa]{background:var(--va-suave)}\n.va-num{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid color-mix(in oklab, var(--va-fg) 35%, transparent);font:600 10.5px/18px var(--va-mono);text-align:center;transition:background .18s,color .18s}\n.va-item[data-ativa] .va-num{background:var(--va-fg);color:var(--va-bg);border-color:var(--va-fg)}\n.va-detalhe{display:grid;grid-template-rows:0fr;transition:grid-template-rows .26s var(--va-ease)}\n.va-detalhe>div{overflow:hidden;padding:0 8px 0 38px;color:var(--va-muted);font-size:13px;line-height:1.5}\n.va-detalhe[data-aberto]{grid-template-rows:1fr}\n.va-detalhe[data-aberto]>div{padding-bottom:10px}\n.va-botao{display:inline-flex;align-items:center;gap:6px;margin-top:8px;padding:7px 12px;border:1px solid var(--va-borda);border-radius:var(--va-raio-c);background:var(--va-bg);font-size:12.5px;font-weight:600;cursor:pointer;transition:background .18s,border-color .18s}\n.va-botao:hover{border-color:color-mix(in oklab, var(--va-fg) 35%, transparent)}\n.va-botao[data-primario]{background:var(--va-brand);color:var(--va-brand-fg);border-color:transparent}\n.va-botao[data-primario]:hover{background:color-mix(in oklab, var(--va-brand) 88%, var(--va-fg))}\n.va-botao:disabled{opacity:.4;cursor:default}\n.va-vazio{margin:0 4px;color:var(--va-muted);font-size:12.5px;line-height:1.5}\n.va-progresso{display:flex;gap:4px;margin:2px 4px}\n.va-progresso button{flex:1;height:4px;padding:0;border:0;border-radius:2px;background:var(--va-borda);cursor:pointer;transition:background .2s}\n.va-progresso button[data-feito]{background:color-mix(in oklab, var(--va-fg) 55%, transparent)}\n.va-progresso button[aria-current=\"step\"]{background:var(--va-fg)}\n.va-passo{margin:0 4px}\n.va-passo h4{margin:6px 0 6px;font-size:15px;font-weight:650;letter-spacing:-.01em}\n.va-passo p{margin:0;color:var(--va-muted);font-size:13.5px;line-height:1.55}\n.va-nav{display:flex;justify-content:space-between;gap:8px;margin:auto 4px 2px}\n.va-resumo{padding:10px 18px 14px;border-top:1px solid var(--va-borda);color:var(--va-muted);font-size:12.5px;line-height:1.5}\n.va-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}\n[hidden]{display:none!important}\n@container (max-width:520px){.va-topo{flex-wrap:wrap}.va-ferramentas{width:100%;justify-content:space-between}.va-palco{padding:18px 10px 40px}.va-pino{width:18px;height:18px;margin:-9px 0 0 -9px;font-size:9.5px;line-height:16px}}\n@media (prefers-reduced-motion:reduce){.va *,.va *::before{transition:none!important;animation:none!important}}\n`;\n\n/* SSR: no servidor não existe `HTMLElement`, e `extends HTMLElement` no topo\n   do módulo derruba a renderização de servidor de toda página que importe este\n   arquivo (visto no curso do Club, 28/10/2026). Fora do navegador a classe\n   estende uma vazia; o `customElements.define` já não roda lá. É o mesmo\n   truque do shim de SSR do Lit. */\nconst ElementoBase = (typeof HTMLElement === \"undefined\" ? class {} : HTMLElement) as typeof HTMLElement;\n\nexport class VisantAnatomia extends ElementoBase {\n  static observedAttributes = [\"figura\", \"modo\"];\n  #fig: FiguraDef | null = null;\n  #controle: Controle | null = null;\n  #raiz: ShadowRoot;\n  #limpar: (() => void)[] = [];\n\n  constructor() {\n    super();\n    this.#raiz = this.attachShadow({ mode: \"open\" });\n  }\n\n  /** Pra React e afins: passar a figura direto, sem registrar. */\n  set figura(def: FiguraDef | string | null) {\n    if (typeof def === \"string\") this.setAttribute(\"figura\", def);\n    else {\n      this.#fig = def;\n      this.montar();\n    }\n  }\n\n  connectedCallback() {\n    this.montar();\n  }\n  disconnectedCallback() {\n    this.desmontar();\n  }\n  attributeChangedCallback(nome: string, antes: string | null, depois: string | null) {\n    if (antes === depois || !this.isConnected) return;\n    if (nome === \"figura\") {\n      this.#fig = null;\n      this.montar();\n    }\n    if (nome === \"modo\") this.#definirModo?.(depois === \"passos\" ? \"passos\" : \"explorar\");\n  }\n\n  #definirModo: ((m: \"explorar\" | \"passos\") => void) | null = null;\n\n  desmontar() {\n    this.#controle?.destruir();\n    this.#controle = null;\n    this.#limpar.forEach((f) => f());\n    this.#limpar = [];\n  }\n\n  montar() {\n    const def = this.#fig ?? figuras.get(this.getAttribute(\"figura\") ?? \"\");\n    if (!def || !this.isConnected) return;\n    this.desmontar();\n    this.#fig = def;\n    const d = def.dados;\n    const raiz = this.#raiz;\n    const passos = d.passos ?? [];\n\n    raiz.innerHTML = `<style>${CSS}${ESTILO_DO_KIT}${d.css ?? \"\"}</style>\n<figure class=\"va\" data-modo=\"explorar\">\n  <header class=\"va-topo\">\n    <div class=\"va-id\"><span class=\"va-fig\">${esc(d.fig)}</span><h3 class=\"va-titulo\">${esc(d.titulo)}</h3></div>\n    <div class=\"va-ferramentas\" role=\"toolbar\" aria-label=\"Controles da figura\">\n      ${passos.length ? `<div class=\"va-modos\" role=\"tablist\" aria-label=\"Modo\"><button role=\"tab\" data-modo=\"explorar\" aria-selected=\"true\">Explorar</button><button role=\"tab\" data-modo=\"passos\" aria-selected=\"false\">Passo a passo</button></div>` : \"\"}\n      <button class=\"va-icone\" data-acao=\"tocar\" aria-label=\"Pausar\" title=\"Pausar\">${icone(\"pausar\")}</button>\n      <button class=\"va-icone\" data-acao=\"reiniciar\" aria-label=\"Recomeçar\" title=\"Recomeçar\">${icone(\"reiniciar\")}</button>\n      <button class=\"va-icone\" data-acao=\"tela\" aria-label=\"Tela cheia\" title=\"Tela cheia\">${icone(\"tela\")}</button>\n    </div>\n  </header>\n  <div class=\"va-corpo\">\n    <div class=\"va-palco va-palco-kit\" tabindex=\"0\" role=\"group\" aria-roledescription=\"figura interativa\" aria-label=\"${esc(d.resumo)} Use as setas pra passar pelas peças e Enter pra usar a peça.\">\n      <div class=\"va-arte\">${d.svg}\n        <div class=\"va-pinos\">${d.pecas.map((p) => `<button class=\"va-pino\" data-n=\"${p.n}\" tabindex=\"-1\" aria-label=\"${p.n}: ${esc(p.nome)}\">${p.n}</button>`).join(\"\")}</div>\n        <div class=\"va-etiqueta\" aria-hidden=\"true\"></div>\n      </div>\n      <output class=\"va-leitura\" aria-live=\"off\">${esc(d.leitura)}</output>\n    </div>\n    <aside class=\"va-painel\">\n      <div class=\"va-explorar\">\n        <p class=\"va-rotulo\">${d.pecas.length} peças</p>\n        <ol class=\"va-lista\">${d.pecas\n          .map(\n            (p) => `<li><button class=\"va-item\" data-n=\"${p.n}\" aria-expanded=\"false\"><span class=\"va-num\">${p.n}</span><span>${esc(p.nome.replace(/\\.$/, \"\"))}</span></button>\n<div class=\"va-detalhe\" data-n=\"${p.n}\"><div><p style=\"margin:0\">${esc(p.texto)}</p>${p.acao ? `<button class=\"va-botao\" data-primario data-acao-peca=\"${p.n}\">${esc(p.acao)}</button>` : \"\"}</div></div></li>`,\n          )\n          .join(\"\")}</ol>\n      </div>\n      ${\n        passos.length\n          ? `<div class=\"va-passos\" hidden>\n        <div class=\"va-progresso\">${passos.map((_, i) => `<button aria-label=\"Passo ${i + 1}\" data-i=\"${i}\"></button>`).join(\"\")}</div>\n        <div class=\"va-passo\"><p class=\"va-rotulo\" data-passo-n></p><h4 data-passo-titulo></h4><p data-passo-texto></p></div>\n        <div class=\"va-nav\"><button class=\"va-botao\" data-nav=\"-1\">Anterior</button><button class=\"va-botao\" data-primario data-nav=\"1\">Próximo</button></div>\n      </div>`\n          : \"\"\n      }\n    </aside>\n  </div>\n  <figcaption class=\"va-resumo\">${esc(d.resumo)}</figcaption>\n  <p class=\"va-sr\" aria-live=\"polite\" data-anuncio></p>\n</figure>`;\n\n    const $ = <T extends Element = HTMLElement>(s: string) => raiz.querySelector(s) as T;\n    const $$ = <T extends Element = HTMLElement>(s: string) => [...raiz.querySelectorAll(s)] as T[];\n    const va = $(\".va\");\n    const palco = $(\".va-palco\");\n    const arte = $(\".va-arte\");\n    const svg = arte.querySelector(\"svg\")!;\n    const vb = svg.viewBox.baseVal;\n    const leitura = $(\".va-leitura\");\n    const etiqueta = $(\".va-etiqueta\");\n    const anuncio = $(\"[data-anuncio]\");\n    const pinos = new Map($$<HTMLButtonElement>(\".va-pino\").map((el) => [Number(el.dataset.n), el]));\n    const grupos = (n: number) => $$<SVGGElement>(`.va-arte svg [data-peca=\"${n}\"]`);\n    const itens = new Map($$<HTMLButtonElement>(\".va-item\").map((el) => [Number(el.dataset.n), el]));\n    const detalhes = new Map($$(\".va-detalhe\").map((el) => [Number(el.dataset.n), el]));\n    const pecasVivas = d.pecas.map((p) => ({ ...p }));\n    const peca = (n: number) => pecasVivas.find((p) => p.n === n)!;\n    const reduzido = matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const ouvir = <K extends keyof HTMLElementEventMap>(el: EventTarget, ev: K | string, fn: (e: any) => void, opt?: AddEventListenerOptions) => {\n      el.addEventListener(ev, fn, opt);\n      this.#limpar.push(() => el.removeEventListener(ev, fn, opt));\n    };\n\n    // ---- estado do foco\n    let modo: \"explorar\" | \"passos\" = \"explorar\";\n    let hover: number | null = null;\n    let selecionada: number | null = null;\n    let passo = 0;\n    const principal = () => selecionada ?? hover;\n    const ativas = (): number[] => {\n      const p = principal();\n      if (p !== null) return [p];\n      return modo === \"passos\" ? passos[passo]?.foco ?? [] : [];\n    };\n\n    // ---- pinos: posição em % do viewBox, então acompanham qualquer largura\n    const posicao = new Map<number, [number, number]>();\n    const escondidos = new Map<number, [number, number]>();\n    const porPino = (n: number, ponto: Ponto3 | null) => {\n      if (ponto === null) {\n        posicao.delete(n);\n        pinos.get(n)?.toggleAttribute(\"hidden\", true);\n        if (n === principal()) mostrarEtiqueta();\n        return;\n      }\n      pinos.get(n)?.removeAttribute(\"hidden\");\n      const [sx, sy] = kit.iso(ponto, d.P as kit.Projection);\n      const x = (sx / vb.width) * 100;\n      const y = (sy / vb.height) * 100;\n      posicao.set(n, [x, y]);\n      const el = pinos.get(n);\n      if (el) {\n        el.style.left = `${x}%`;\n        el.style.top = `${y}%`;\n      }\n      if (n === principal()) mostrarEtiqueta();\n    };\n    d.pecas.forEach((p) => porPino(p.n, p.ancora));\n\n    function mostrarEtiqueta() {\n      const n = principal();\n      const pos = n !== null ? posicao.get(n) : undefined;\n      if (n === null || !pos) return etiqueta.removeAttribute(\"data-visivel\");\n      etiqueta.textContent = peca(n).nome.replace(/\\.$/, \"\");\n      etiqueta.style.left = `${pos[0]}%`;\n      etiqueta.style.top = `${pos[1]}%`;\n      etiqueta.setAttribute(\"data-visivel\", \"\");\n    }\n\n    const aplicar = () => {\n      const lista = ativas();\n      va.toggleAttribute(\"data-focando\", lista.length > 0);\n      const junto = lista.length === 1 ? peca(lista[0])?.junto ?? [] : [];\n      $$(\".va-arte svg [data-peca]\").forEach((g) => {\n        const m = Number(g.getAttribute(\"data-peca\"));\n        g.toggleAttribute(\"data-ativa\", lista.includes(m));\n        g.toggleAttribute(\"data-junto\", junto.includes(m));\n      });\n      pinos.forEach((el, n) => el.toggleAttribute(\"data-ativa\", lista.includes(n)));\n      const p = principal();\n      itens.forEach((el, n) => {\n        el.toggleAttribute(\"data-ativa\", n === p);\n        el.setAttribute(\"aria-expanded\", String(n === p));\n      });\n      detalhes.forEach((el, n) => el.toggleAttribute(\"data-aberto\", n === p));\n      mostrarEtiqueta();\n      refrescarAcao();\n    };\n\n    function refrescarAcao() {\n      const p = principal();\n      const acao = p !== null ? controle?.acoes?.[p] : undefined;\n      const botao = p !== null ? raiz.querySelector<HTMLButtonElement>(`[data-acao-peca=\"${p}\"]`) : null;\n      if (!acao || !botao) return;\n      const rotulo = acao.rotulo();\n      if (botao.textContent !== rotulo) botao.textContent = rotulo;\n      botao.disabled = acao.ativa ? !acao.ativa() : false;\n    }\n\n    const anunciar = (texto: string) => {\n      anuncio.textContent = texto;\n    };\n\n    // ---- foco inteligente: ímã do pino, peça embaixo, intenção e histerese\n    let espera = 0;\n    let saida = 0;\n    const IMA_FORTE = 18;\n    const IMA = 46;\n    function candidatoEm(e: PointerEvent): number | null {\n      const caixa = arte.getBoundingClientRect();\n      const px = e.clientX - caixa.left;\n      const py = e.clientY - caixa.top;\n      let perto: number | null = null;\n      let dist = Infinity;\n      posicao.forEach(([x, y], n) => {\n        const dd = Math.hypot((x / 100) * caixa.width - px, (y / 100) * caixa.height - py);\n        if (dd < dist) {\n          dist = dd;\n          perto = n;\n        }\n      });\n      if (perto !== null && dist <= IMA_FORTE) return perto;\n      const alvo = (e.composedPath()[0] as Element | undefined)?.closest?.(\"[data-peca]\");\n      const n = alvo ? Number(alvo.getAttribute(\"data-peca\")) : 0;\n      if (n > 0 && pinos.has(n)) return n;\n      return perto !== null && dist <= IMA ? perto : null;\n    }\n    function dentroDaAtual(e: PointerEvent) {\n      if (hover === null) return false;\n      return grupos(hover).some((g) => {\n        const r = g.getBoundingClientRect();\n        return e.clientX >= r.left - 8 && e.clientX <= r.right + 8 && e.clientY >= r.top - 8 && e.clientY <= r.bottom + 8;\n      });\n    }\n    const trocarHover = (n: number | null) => {\n      if (n === hover) return;\n      hover = n;\n      aplicar();\n    };\n    ouvir(arte, \"pointermove\", (e: PointerEvent) => {\n      if (e.pointerType === \"touch\") return;\n      clearTimeout(saida);\n      va.setAttribute(\"data-interagindo\", \"\");\n      const n = candidatoEm(e);\n      clearTimeout(espera);\n      if (n === hover) return;\n      if (hover === null) return trocarHover(n);\n      if (n === null && dentroDaAtual(e)) return;\n      espera = window.setTimeout(() => trocarHover(n), 90);\n    });\n    ouvir(arte, \"pointerleave\", () => {\n      clearTimeout(espera);\n      saida = window.setTimeout(() => {\n        trocarHover(null);\n        if (!palco.matches(\":focus-within\")) va.removeAttribute(\"data-interagindo\");\n      }, 180);\n    });\n    ouvir(arte, \"click\", (e: PointerEvent) => {\n      const n = candidatoEm(e);\n      selecionar(n === selecionada ? null : n, { rolar: true });\n    });\n    pinos.forEach((el, n) => ouvir(el, \"click\", (e: Event) => {\n      e.stopPropagation();\n      selecionar(n === selecionada ? null : n, { rolar: true });\n    }));\n\n    function selecionar(n: number | null, { rolar = false } = {}) {\n      selecionada = n;\n      if (n !== null && modo === \"passos\") definirModo(\"explorar\");\n      aplicar();\n      if (n !== null) {\n        anunciar(`${n}. ${peca(n).nome} ${peca(n).texto}`);\n        if (rolar) itens.get(n)?.scrollIntoView({ block: \"nearest\", behavior: reduzido.matches ? \"auto\" : \"smooth\" });\n      }\n    }\n\n    // ---- painel\n    itens.forEach((el, n) => {\n      ouvir(el, \"pointerenter\", () => trocarHover(n));\n      ouvir(el, \"pointerleave\", () => trocarHover(null));\n      ouvir(el, \"click\", () => selecionar(n === selecionada ? null : n));\n    });\n    $$<HTMLButtonElement>(\"[data-acao-peca]\").forEach((b) =>\n      ouvir(b, \"click\", (e: Event) => {\n        e.stopPropagation();\n        const n = Number(b.dataset.acaoPeca);\n        controle?.acoes?.[n]?.rodar();\n        aplicar();\n      }),\n    );\n\n    // ---- passo a passo\n    const passosEl = raiz.querySelector<HTMLElement>(\".va-passos\");\n    const explorarEl = $(\".va-explorar\");\n    function irPasso(i: number) {\n      passo = Math.max(0, Math.min(passos.length - 1, i));\n      const p = passos[passo];\n      $(\"[data-passo-n]\").textContent = `Passo ${passo + 1} de ${passos.length}`;\n      $(\"[data-passo-titulo]\").textContent = p.titulo;\n      $(\"[data-passo-texto]\").textContent = p.texto;\n      $$<HTMLButtonElement>(\".va-progresso button\").forEach((b, j) => {\n        b.toggleAttribute(\"data-feito\", j < passo);\n        if (j === passo) b.setAttribute(\"aria-current\", \"step\");\n        else b.removeAttribute(\"aria-current\");\n      });\n      raiz.querySelector<HTMLButtonElement>('[data-nav=\"-1\"]')!.disabled = passo === 0;\n      raiz.querySelector<HTMLButtonElement>('[data-nav=\"1\"]')!.textContent = passo === passos.length - 1 ? \"Explorar\" : \"Próximo\";\n      controle?.irParaPasso?.(passo);\n      aplicar();\n      anunciar(`Passo ${passo + 1} de ${passos.length}. ${p.titulo}. ${p.texto}`);\n    }\n    function definirModo(m: \"explorar\" | \"passos\") {\n      if (!passosEl || m === modo) return;\n      modo = m;\n      va.dataset.modo = m;\n      passosEl.hidden = m !== \"passos\";\n      explorarEl.hidden = m === \"passos\";\n      $$<HTMLButtonElement>(\".va-modos button\").forEach((b) => b.setAttribute(\"aria-selected\", String(b.dataset.modo === m)));\n      if (m === \"passos\") {\n        selecionada = null;\n        hover = null;\n        irPasso(0);\n      } else {\n        controle?.tocar();\n        aplicar();\n      }\n    }\n    this.#definirModo = definirModo;\n    $$<HTMLButtonElement>(\".va-modos button\").forEach((b) => ouvir(b, \"click\", () => definirModo(b.dataset.modo as \"explorar\" | \"passos\")));\n    $$<HTMLButtonElement>(\"[data-nav]\").forEach((b) =>\n      ouvir(b, \"click\", () => {\n        const ultimo = passo === passos.length - 1;\n        if (b.dataset.nav === \"1\" && ultimo) return definirModo(\"explorar\");\n        irPasso(passo + Number(b.dataset.nav));\n      }),\n    );\n    $$<HTMLButtonElement>(\".va-progresso button\").forEach((b) => ouvir(b, \"click\", () => irPasso(Number(b.dataset.i))));\n\n    // ---- teclado no palco\n    const ordem = d.pecas.map((p) => p.n);\n    ouvir(palco, \"focus\", () => va.setAttribute(\"data-interagindo\", \"\"));\n    ouvir(palco, \"blur\", () => va.removeAttribute(\"data-interagindo\"));\n    ouvir(palco, \"keydown\", (e: KeyboardEvent) => {\n      if (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") {\n        e.preventDefault();\n        const passoTecla = e.key === \"ArrowRight\" ? 1 : -1;\n        if (modo === \"passos\") return irPasso(passo + passoTecla);\n        const i = selecionada === null ? (passoTecla > 0 ? -1 : 0) : ordem.indexOf(selecionada);\n        selecionar(ordem[(i + passoTecla + ordem.length) % ordem.length], { rolar: true });\n      } else if (e.key === \"Enter\" && selecionada !== null && controle?.acoes?.[selecionada]) {\n        e.preventDefault();\n        controle.acoes[selecionada].rodar();\n        aplicar();\n      } else if (e.key === \"Escape\") {\n        selecionar(null);\n      }\n    });\n\n    // ---- laço único, só com a figura na tela e sem pausa\n    let visivel = false;\n    let pausado = false;\n    let quadro = 0;\n    let ultimo = 0;\n    const lacos = new Set<{ ativo: boolean; tick: (dt: number) => boolean }>();\n    const passar = (agora: number) => {\n      const dt = ultimo ? Math.min((agora - ultimo) / 1000, 1 / 20) : 1 / 60;\n      ultimo = agora;\n      lacos.forEach((l) => {\n        if (l.ativo && !l.tick(dt)) l.ativo = false;\n      });\n      quadro = visivel && !pausado && [...lacos].some((l) => l.ativo) ? requestAnimationFrame(passar) : 0;\n    };\n    const acordar = () => {\n      if (!quadro && visivel && !pausado && !reduzido.matches && [...lacos].some((l) => l.ativo)) {\n        ultimo = 0;\n        quadro = requestAnimationFrame(passar);\n      }\n    };\n    const io = new IntersectionObserver((es) => {\n      visivel = es[es.length - 1].isIntersecting;\n      if (visivel) acordar();\n    }, { rootMargin: \"120px 0px\" });\n    io.observe(this);\n    this.#limpar.push(() => {\n      io.disconnect();\n      cancelAnimationFrame(quadro);\n      clearTimeout(espera);\n      clearTimeout(saida);\n    });\n\n    const api: AnatomiaApi = {\n      kit,\n      P: d.P,\n      dizer(texto) {\n        if (leitura.textContent === texto) return;\n        leitura.textContent = texto;\n        // o rótulo da ação costuma depender do mesmo estado que a leitura conta\n        refrescarAcao();\n      },\n      moverPino: porPino,\n      reduzido: () => reduzido.matches,\n      mostrarPino(n, visivel) {\n        const el = pinos.get(n);\n        if (!el) return;\n        el.toggleAttribute(\"hidden\", !visivel);\n        if (!visivel) {\n          const pos = posicao.get(n);\n          if (pos) escondidos.set(n, pos);\n          posicao.delete(n);\n        } else if (escondidos.has(n)) {\n          posicao.set(n, escondidos.get(n)!);\n          escondidos.delete(n);\n        }\n        if (n === principal()) mostrarEtiqueta();\n      },\n      textoDaPeca(n, texto) {\n        const p = peca(n);\n        if (!p || p.texto === texto) return;\n        p.texto = texto;\n        const alvo = raiz.querySelector(`.va-detalhe[data-n=\"${n}\"] p`);\n        if (alvo) alvo.textContent = texto;\n      },\n      atualizar: () => aplicar(),\n      loop(tick) {\n        const l = { ativo: false, tick };\n        lacos.add(l);\n        return {\n          iniciar() {\n            l.ativo = true;\n            acordar();\n          },\n          parar() {\n            l.ativo = false;\n          },\n        };\n      },\n    };\n\n    // ---- controles do topo\n    const botaoTocar = raiz.querySelector<HTMLButtonElement>('[data-acao=\"tocar\"]')!;\n    const marcarTocar = () => {\n      botaoTocar.innerHTML = icone(pausado ? \"tocar\" : \"pausar\");\n      botaoTocar.setAttribute(\"aria-label\", pausado ? \"Tocar\" : \"Pausar\");\n      botaoTocar.title = pausado ? \"Tocar\" : \"Pausar\";\n    };\n    ouvir(botaoTocar, \"click\", () => {\n      pausado = !pausado;\n      marcarTocar();\n      if (pausado) controle?.pausar();\n      else {\n        controle?.tocar();\n        acordar();\n      }\n    });\n    ouvir(raiz.querySelector('[data-acao=\"reiniciar\"]')!, \"click\", () => {\n      pausado = false;\n      marcarTocar();\n      controle?.reiniciar();\n      acordar();\n    });\n    ouvir(raiz.querySelector('[data-acao=\"tela\"]')!, \"click\", () => {\n      if (document.fullscreenElement === this) document.exitFullscreen();\n      else this.requestFullscreen?.().catch(() => {});\n    });\n    if (!document.fullscreenEnabled) raiz.querySelector<HTMLElement>('[data-acao=\"tela\"]')!.hidden = true;\n\n    const controle: Controle = def.controlador(arte, d.modelo, api);\n    this.#controle = controle;\n    if (!reduzido.matches) controle.tocar();\n    aplicar();\n    if (this.getAttribute(\"modo\") === \"passos\") definirModo(\"passos\");\n  }\n}\n\n/** Registra o `<visant-anatomia>` (uma vez só, mesmo chamado várias vezes). */\nexport function definirAnatomia(tag = \"visant-anatomia\") {\n  if (typeof customElements !== \"undefined\" && !customElements.get(tag)) customElements.define(tag, VisantAnatomia);\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/visant/ui/anatomia.tsx",
      "content": "\"use client\";\n\n/**\n * Anatomia: a figura didática interativa num componente React.\n *\n *   <Anatomia figura={comoSobe} />   (comoSobe vem do item @visant/anatomia-como-sobe)\n *\n * Por baixo é o web component `<visant-anatomia>` (Shadow DOM), então a mesma\n * figura roda em React, em página estática, em livro e em blog. O wrapper só\n * registra o elemento e entrega a figura como propriedade; o tema vem das\n * variáveis do site (--foreground, --card, --brand...).\n */\nimport * as React from \"react\";\n\nimport { definirAnatomia, type FiguraDef, type VisantAnatomia } from \"@/lib/anatomia-nucleo\";\n\nexport interface AnatomiaProps extends React.HTMLAttributes<HTMLElement> {\n  figura: FiguraDef;\n  /** Começa explorando (padrão) ou já no passo a passo. */\n  modo?: \"explorar\" | \"passos\";\n  /** Só pra site sem tokens: força o esquema das cores do sistema. */\n  tema?: \"claro\" | \"escuro\";\n}\n\nexport function Anatomia({ figura, modo, tema, ...rest }: AnatomiaProps) {\n  const ref = React.useRef<VisantAnatomia | null>(null);\n  React.useEffect(() => {\n    definirAnatomia();\n    // Propriedade e não atributo: a figura é um objeto, e assim não precisa\n    // de registro global nem colide com outra figura de mesmo id.\n    if (ref.current) ref.current.figura = figura;\n  }, [figura]);\n  return React.createElement(\"visant-anatomia\", { ref, modo, tema, ...rest });\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Montar figura didática interativa com pinos e passo a passo; é a base de toda anatomia-*",
      "evite": "Diagrama estático ou explicação sem interação: imagem ou SVG simples"
    }
  },
  "categories": [
    "anatomia",
    "didatica",
    "aula",
    "isometrico",
    "explicacao",
    "web-component",
    "passo-a-passo"
  ],
  "type": "registry:ui"
}