{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "streamgraph",
  "title": "Streamgraph",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Gráfico de várias partes no tempo em sete estilos: fluxo, empilhado, 100%, cristas, linhas, barras e centrado, com camadas que ligam e desligam.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/visant/ui/streamgraph.tsx",
      "content": "\"use client\";\n\n/* Adaptado de Streamgraph — https://uiarc.dev/components/streamgraph — Copyright (c) 2026 Elia Kuratli, licença MIT (texto integral em ATTRIBUTIONS.md do @visant/ui). */\n\nimport * as React from \"react\";\nimport { animate, motion, useInView, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Streamgraph — como a mistura de várias partes muda no tempo.\n *\n * Tíquete de suporte por assunto, receita por canal, hora por cliente. Quando o\n * que importa é a FORMA do conjunto (qual parte engordou, quando a mistura\n * virou) mais que o valor exato de cada semana, a pilha em fluxo conta isso de\n * uma olhada. Valor exato mora na dica e na tabela pro leitor de tela.\n *\n * ── O que não é óbvio ──────────────────────────────────────────────────────\n *\n * 1. **Cor é exceção.** A primeira camada (o centro do fluxo) veste a marca;\n *    as outras são degraus de neutro que alternam claro e escuro, então\n *    vizinhas nunca se confundem. A camada em foco ganha a cor cheia e as\n *    outras recuam. Texto nunca usa a cor da série.\n * 2. **Toda camada é reamostrada na mesma grade de 161 pontos**, com tangente\n *    monotônica: a curva nunca passa do dado nem desce de zero, e trocar de\n *    período com mais ou menos pontos é um morph, não um corte.\n * 3. **\"wiggle\" segue Byron e Wattenberg** (minimiza a inclinação das camadas)\n *    e tira a deriva linear depois, pra o fluxo não sair inclinado do quadro.\n * 4. **Um pintor só.** Morph de dado, camada ligando e desligando, o inchaço da\n *    primeira aparição e o redimensionamento passam todos pela mesma função,\n *    que escreve direto no `d` dos paths, sem render do React por quadro.\n * 5. **Ponteiro sob escala.** A vitrine escala o palco; a posição vertical do\n *    ponteiro é dividida pela escala antes de procurar a camada, senão a dica\n *    aponta a camada errada no ladrilho.\n * 6. **Sete estilos, um pintor.** Fluxo, centrado, empilhado e 100% mudam a base\n *    (`offset`); cristas dá uma linha de base a cada camada; `shape` troca o\n *    desenho por linhas ou barras. Dado, dica, teclado e legenda são os mesmos.\n * 7. **Teclado é cidadão.** No gráfico, ←/→ andam no tempo, PageUp/PageDown\n *    pulam um sexto, Home/End vão às pontas, ↑/↓ trocam de camada, Esc sai.\n *    A legenda liga e desliga camadas, e a camada desligada afina até sumir\n *    enquanto as outras fecham por cima.\n *\n * Vazio tem desenho (a frase no lugar do fluxo), e nenhuma camada ligada\n * também. Com `prefers-reduced-motion` tudo chega no estado final, sem viagem.\n */\n\nexport interface StreamgraphSeries {\n  /** Identidade estável. A camada guarda lugar e cor entre trocas de dado. */\n  key: string;\n  /** Nome na legenda, no rótulo direto, na dica e na tabela. */\n  label: string;\n  /** Qualquer cor CSS. Sem ela: a primeira veste a marca, as outras são neutros. */\n  color?: string;\n}\n\nexport interface StreamgraphDatum {\n  /** Identidade estável, como a semana ISO. */\n  key: string;\n  /** Rótulo completo, pra dica e leitor de tela (\"Semana de 14 de set.\"). */\n  label: string;\n  /** Rótulo curto no eixo. Sem ele, o eixo fica quieto nesse ponto. */\n  axisLabel?: string;\n  /** Um valor por chave de série. Faltando conta zero; negativo vira zero. */\n  values: Record<string, number | undefined>;\n}\n\nexport interface StreamgraphProps {\n  data: StreamgraphDatum[];\n  /** Camadas do centro pra fora: a primeira corre no meio, as outras alternam acima e abaixo. */\n  series: StreamgraphSeries[];\n  /** O que se mede, como \"Tíquetes por assunto\". Nomeia o gráfico. */\n  label: string;\n  /** Unidade depois de cada valor, como \"tíquetes\". */\n  unit?: string;\n  /** Altura do gráfico em px. A largura segue o contêiner. */\n  height?: number;\n  /** \"wiggle\" flui, \"silhouette\" centra, \"zero\" empilha, \"expand\" mostra em 100%, \"ridge\" dá uma base a cada camada. */\n  offset?: \"wiggle\" | \"silhouette\" | \"zero\" | \"expand\" | \"ridge\";\n  /** \"area\" pinta as camadas, \"line\" desenha só a linha de cada uma, \"bar\" empilha em colunas. */\n  shape?: \"area\" | \"line\" | \"bar\";\n  formatValue?: (value: number, series: StreamgraphSeries) => string;\n  /** Camadas escondidas, controlado. */\n  hiddenSeries?: string[];\n  defaultHiddenSeries?: string[];\n  onHiddenSeriesChange?: (hidden: string[]) => void;\n  /** Mostra a legenda que liga e desliga camadas. */\n  legend?: boolean;\n  /** Escreve o nome de cada camada no trecho mais grosso dela, quando cabe. */\n  directLabels?: boolean;\n  /** Cabeçalho da primeira coluna da tabela do leitor de tela. */\n  categoryLabel?: string;\n  emptyLabel?: string;\n  className?: string;\n}\n\ntype Hover = { index: number; key: string | null; y: number | null };\ntype Quadro = { lower: number[][]; upper: number[][]; toY: (v: number) => number };\n\n/** Mola de período fixo convertida em rigidez e amortecimento, pra animar números soltos. */\nconst fisica = (visualDuration: number, bounce: number, restDelta = 0.0005) => {\n  const raiz = (2 * Math.PI) / (visualDuration * 1.2);\n  return { type: \"spring\" as const, stiffness: raiz * raiz, damping: 2 * (1 - bounce) * raiz, restDelta, restSpeed: restDelta * 2 };\n};\n// Assentar responde a clique na legenda: 300 ms. O inchaço é de entrada de dobra, pode mais.\nconst ASSENTA = fisica(0.3, 0);\nconst INCHA = fisica(0.9, 0);\nconst DESLIZA = fisica(0.26, 0.12, 0.01);\nconst SEGUE = { stiffness: DESLIZA.stiffness, damping: DESLIZA.damping, restDelta: 0.01 };\n\nconst N = 161;\nconst MARGEM = 10;\nconst VAO = 14;\n// Degraus de neutro pras camadas sem cor própria, alternando claro e escuro.\nconst DEGRAUS = [34, 18, 27, 12, 40, 22];\n// Linha é fina e pede mais contraste que área: os mesmos neutros, mais fortes, e a\n// marca puxada pro texto (no branco ela sozinha não aparece).\nconst DEGRAUS_LINHA = [72, 46, 60, 36, 84, 52];\nconst corDe = (s: StreamgraphSeries, i: number, linha = false) =>\n  s.color ??\n  (i === 0\n    ? linha\n      ? \"color-mix(in oklab, var(--brand) 78%, var(--foreground))\"\n      : \"var(--brand)\"\n    : `color-mix(in oklab, var(--foreground) ${(linha ? DEGRAUS_LINHA : DEGRAUS)[(i - 1) % DEGRAUS.length]}%, var(--card))`);\n\n/** Linha não empilha, e barra só faz sentido de base reta. */\nconst baseDe = (offset: StreamgraphProps[\"offset\"], shape: StreamgraphProps[\"shape\"]) =>\n  shape === \"line\" || (shape === \"bar\" && offset !== \"expand\") ? \"zero\" : offset;\n\n/** A primeira camada no meio, as outras alternando acima e abaixo; base reta empilha em ordem. */\nfunction ordemDaPilha(n: number, modo: StreamgraphProps[\"offset\"]) {\n  const ordem = Array.from({ length: n }, (_, i) => i);\n  if (modo === \"zero\" || modo === \"expand\" || modo === \"ridge\") return ordem;\n  return ordem.reduce<number[]>((l, i) => (i === 0 ? [0] : i % 2 ? [...l, i] : [i, ...l]), []);\n}\nconst agrupado = new Intl.NumberFormat(\"pt-BR\", { maximumFractionDigits: 1 });\nconst limitar = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\n\n/** Reamostra na grade comum com tangente monotônica (nota 2). */\nfunction reamostrar(cru: number[]): number[] {\n  const v = cru.map((x) => Math.max(0, x || 0));\n  const out = new Array<number>(N);\n  if (!v.length) return out.fill(0);\n  if (v.length === 1) return out.fill(v[0]!);\n  const ult = v.length - 1;\n  const inc = v.slice(1).map((x, i) => x - v[i]!);\n  const tg = v.map((_, i) =>\n    i === 0 || i === ult\n      ? 0\n      : (Math.sign(inc[i - 1]!) + Math.sign(inc[i]!)) *\n          Math.min(Math.abs(inc[i - 1]!), Math.abs(inc[i]!), Math.abs(inc[i - 1]! + inc[i]!) / 4) || 0,\n  );\n  if (ult > 1) {\n    tg[0] = (3 * inc[0]! - tg[1]!) / 2;\n    tg[ult] = (3 * inc[ult - 1]! - tg[ult - 1]!) / 2;\n  } else {\n    tg[0] = tg[1] = inc[0]!;\n  }\n  for (let k = 0; k < N; k++) {\n    const at = (k / (N - 1)) * ult;\n    const seg = Math.min(ult - 1, Math.floor(at));\n    const t = at - seg;\n    const t2 = t * t;\n    const t3 = t2 * t;\n    const a = v[seg]!;\n    const b = v[seg + 1]!;\n    const x = (2 * t3 - 3 * t2 + 1) * a + (t3 - 2 * t2 + t) * tg[seg]! + (3 * t2 - 2 * t3) * b + (t3 - t2) * tg[seg + 1]!;\n    out[k] = limitar(x, Math.min(a, b), Math.max(a, b));\n  }\n  return out;\n}\n\n/** A base embaixo da pilha (nota 3). */\nfunction base(camadas: number[][], modo: StreamgraphProps[\"offset\"]): number[] {\n  const g = new Array<number>(N).fill(0);\n  const totais = g.map((_, j) => camadas.reduce((s, c) => s + c[j]!, 0));\n  if (modo === \"zero\" || modo === \"expand\" || modo === \"ridge\" || !camadas.length) return g;\n  if (modo === \"silhouette\") return totais.map((t) => -t / 2);\n  for (let j = 1; j < N; j++) {\n    let s1 = 0;\n    let s2 = 0;\n    let abaixo = 0;\n    for (const c of camadas) {\n      const d = c[j]! - c[j - 1]!;\n      s1 += c[j]!;\n      s2 += (abaixo + d / 2) * c[j]!;\n      abaixo += d;\n    }\n    g[j] = g[j - 1]! - (s1 ? s2 / s1 : 0);\n  }\n  // Reta de mínimos quadrados pelo centro do fluxo, subtraída pra ele não tombar.\n  const centros = g.map((x, j) => x + totais[j]! / 2);\n  const mx = (N - 1) / 2;\n  const my = centros.reduce((s, x) => s + x, 0) / N;\n  let num = 0;\n  let den = 0;\n  centros.forEach((x, j) => {\n    num += (j - mx) * (x - my);\n    den += (j - mx) ** 2;\n  });\n  const incl = den ? num / den : 0;\n  return g.map((x, j) => x - (my + incl * (j - mx)));\n}\n\nconst nada = () => () => {};\nfunction useReduzir() {\n  const hidratado = React.useSyncExternalStore(nada, () => true, () => false);\n  return !!useReducedMotion() && hidratado;\n}\n\n/** Rótulos de eixo que cabem: no máximo um a cada 72 px, contados do mais recente. */\nfunction rotulosDoEixo(data: StreamgraphDatum[], largura: number) {\n  const com = data.flatMap((d, i) => (d.axisLabel ? [i] : []));\n  if (!largura || !com.length) return [];\n  const passo = Math.ceil(com.length / Math.max(2, Math.floor(largura / 72)));\n  return com.reverse().filter((_, r) => r % passo === 0).reverse();\n}\n\nconst transicaoRapida = \"[transition:opacity_var(--dur-fast)_var(--ease-out)]\";\n\nexport function Streamgraph({\n  data,\n  series,\n  label,\n  unit = \"\",\n  height = 260,\n  offset = \"wiggle\",\n  shape = \"area\",\n  formatValue,\n  hiddenSeries,\n  defaultHiddenSeries,\n  onHiddenSeriesChange,\n  legend = true,\n  directLabels = true,\n  categoryLabel = \"Data\",\n  emptyLabel = \"Sem dados neste período\",\n  className,\n}: StreamgraphProps) {\n  const reduzir = useReduzir();\n  const figura = React.useRef<HTMLElement>(null);\n  const plot = React.useRef<HTMLDivElement>(null);\n  const dica = React.useRef<HTMLDivElement>(null);\n  const caminhos = React.useRef(new Map<string, SVGPathElement>());\n  const rotulos = React.useRef(new Map<string, SVGTextElement>());\n  const naTela = useInView(figura, { once: true, amount: 0.3 });\n  const ult = data.length - 1;\n  const vazio = data.length === 0;\n\n  const [escondidasProprias, setEscondidasProprias] = React.useState<string[]>(defaultHiddenSeries ?? []);\n  const escondidas = hiddenSeries ?? escondidasProprias;\n  const alternar = (key: string) => {\n    const prox = escondidas.includes(key) ? escondidas.filter((k) => k !== key) : [...escondidas, key];\n    if (hiddenSeries === undefined) setEscondidasProprias(prox);\n    onHiddenSeriesChange?.(prox);\n  };\n  const visiveis = series.filter((s) => !escondidas.includes(s.key));\n\n  // Cada camada vira curva densa na grade; o pintor mistura do que está na tela até ela.\n  const assinatura = `${series.map((s) => s.key).join(\",\")}|${data\n    .map((d) => series.map((s) => d.values[s.key] ?? 0).join(\",\"))\n    .join(\";\")}`;\n  const alvos = React.useMemo(\n    () => new Map(series.map((s) => [s.key, reamostrar(data.map((d) => d.values[s.key] ?? 0))])),\n    // A assinatura é o conteúdo: arrays novos com o mesmo dado não refazem a curva.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [assinatura],\n  );\n\n  const [hover, setHover] = React.useState<Hover | null>(null);\n  const [assinaturaVista, setAssinaturaVista] = React.useState(assinatura);\n  if (assinaturaVista !== assinatura) {\n    setAssinaturaVista(assinatura);\n    setHover(null);\n  }\n  const indice = hover && !vazio ? Math.min(hover.index, ult) : null;\n  const [chaveLegenda, setChaveLegenda] = React.useState<string | null>(null);\n  const querida = hover?.key ?? chaveLegenda;\n  const ativa = querida && visiveis.some((s) => s.key === querida) ? querida : null;\n\n  const largura = React.useRef(0);\n  const [larguraPlot, setLarguraPlot] = React.useState(0);\n  const formas = React.useRef(new Map<string, number[]>());\n  const presenca = React.useRef(new Map<string, { value: number; stop: (() => void) | null }>());\n  const quadro = React.useRef<Quadro | null>(null);\n  const lugares = React.useRef(new Map<string, number>());\n  const cresce = useMotionValue(0);\n  const modoAtual = baseDe(offset, shape);\n  const vivo = React.useRef({ series, offset: modoAtual, shape, pontos: data.length });\n  React.useLayoutEffect(() => {\n    vivo.current = { series, offset: modoAtual, shape, pontos: data.length };\n  });\n\n  // O pintor (nota 4).\n  const pintar = React.useCallback(() => {\n    const w = largura.current;\n    if (!w) return;\n    const { series: ordem, offset: modo, shape: forma, pontos } = vivo.current;\n    let camadas = ordem.map((s) => {\n      const f = formas.current.get(s.key);\n      const p = presenca.current.get(s.key)?.value ?? 1;\n      return f ? f.map((v) => v * p) : new Array<number>(N).fill(0);\n    });\n    if (modo === \"expand\") {\n      const somas = new Array<number>(N).fill(0).map((_, j) => camadas.reduce((t, c) => t + c[j]!, 0));\n      camadas = camadas.map((c) => c.map((v, j) => (somas[j]! ? v / somas[j]! : 0)));\n    }\n    const pilha = ordemDaPilha(ordem.length, modo);\n    const g = base(pilha.map((i) => camadas[i]!), modo);\n    const lower: number[][] = new Array(ordem.length);\n    const upper: number[][] = new Array(ordem.length);\n    let corre = g.slice();\n    if (forma === \"line\" || modo === \"ridge\") {\n      // Sem pilha: cada camada na sua base. As cristas sobem de baixo pra cima, e a\n      // camada desligada fecha o vão porque o degrau também é multiplicado pela presença.\n      const pico = Math.max(1e-9, ...camadas.flat());\n      let degrau = 0;\n      for (let i = ordem.length - 1; i >= 0; i--) {\n        const c = camadas[i]!;\n        const fundo = new Array<number>(N).fill(forma === \"line\" ? 0 : degrau);\n        lower[i] = fundo;\n        upper[i] = c.map((v, j) => fundo[j]! + v);\n        corre = corre.map((v, j) => Math.max(v, upper[i]![j]!));\n        degrau += pico * 0.42 * (presenca.current.get(ordem[i]!.key)?.value ?? 1);\n      }\n    } else {\n      for (const i of pilha) {\n        const c = camadas[i]!;\n        lower[i] = corre;\n        corre = corre.map((v, j) => v + c[j]!);\n        upper[i] = corre;\n      }\n    }\n    let lo = Infinity;\n    let hi = -Infinity;\n    for (let j = 0; j < N; j++) {\n      lo = Math.min(lo, g[j]!);\n      hi = Math.max(hi, corre[j]!);\n    }\n    if (!Number.isFinite(lo) || hi - lo < 1e-9) {\n      lo = -1;\n      hi = 1;\n    }\n    const vao = hi - lo;\n    const s = cresce.get();\n    const centro = g.map((v, j) => (v + corre[j]!) / 2);\n    const toY = (v: number) => MARGEM + (1 - (v - lo) / vao) * (height - MARGEM * 2);\n    // O inchaço parte de um fio no centro do fluxo.\n    const yEm = (v: number, j: number) => toY(centro[j]! + (v - centro[j]!) * s);\n    quadro.current = { lower, upper, toY };\n    const xs = Array.from({ length: N }, (_, j) => (j / (N - 1)) * w);\n\n    ordem.forEach((serie, i) => {\n      const topo = upper[i]!;\n      const fundo = lower[i]!;\n      const no = caminhos.current.get(serie.key);\n      if (no) {\n        let d = \"\";\n        if (forma === \"bar\") {\n          const coluna = (w / Math.max(1, pontos)) * 0.72;\n          for (let k = 0; k < pontos; k++) {\n            const j = pontos > 1 ? Math.round((k / (pontos - 1)) * (N - 1)) : 0;\n            const cima = yEm(topo[j]!, j);\n            const baixo = yEm(fundo[j]!, j);\n            if (baixo - cima < 0.5) continue;\n            const esq = ((k + 0.5) / pontos) * w - coluna / 2;\n            d += `M${esq.toFixed(1)},${cima.toFixed(1)}H${(esq + coluna).toFixed(1)}V${baixo.toFixed(1)}H${esq.toFixed(1)}Z`;\n          }\n        } else {\n          for (let j = 0; j < N; j++) d += `${j ? \"L\" : \"M\"}${xs[j]!.toFixed(1)},${yEm(topo[j]!, j).toFixed(1)}`;\n          if (forma === \"area\") {\n            for (let j = N - 1; j >= 0; j--) d += `L${xs[j]!.toFixed(1)},${yEm(fundo[j]!, j).toFixed(1)}`;\n            d += \"Z\";\n          }\n        }\n        no.setAttribute(\"d\", d);\n      }\n      // O rótulo senta onde a camada é mais grossa, e segura o lugar até outro\n      // trecho ficar CLARAMENTE mais grosso: nada de rótulo pulando no morph.\n      const texto = rotulos.current.get(serie.key);\n      if (!texto) return;\n      let melhor = 4;\n      for (let j = 4; j < N - 4; j++) if (topo[j]! - fundo[j]! > topo[melhor]! - fundo[melhor]!) melhor = j;\n      const preso = lugares.current.get(serie.key);\n      const j =\n        preso !== undefined && topo[preso]! - fundo[preso]! >= (topo[melhor]! - fundo[melhor]!) * 0.8 ? preso : melhor;\n      lugares.current.set(serie.key, j);\n      const grossura = ((topo[j]! - fundo[j]!) / vao) * (height - MARGEM * 2) * s;\n      const espaco = texto.getComputedTextLength?.() ?? 60;\n      const x = limitar(xs[j]!, espaco / 2 + 6, w - espaco / 2 - 6);\n      texto.setAttribute(\"x\", x.toFixed(1));\n      texto.setAttribute(\"y\", yEm((topo[j]! + fundo[j]!) / 2, j).toFixed(1));\n      texto.style.opacity = grossura >= 19 && espaco + 16 < w ? \"\" : \"0\";\n    });\n  }, [cresce, height]);\n\n  React.useLayoutEffect(() => {\n    const no = plot.current;\n    if (!no) return;\n    // clientWidth é medida local: o palco escalado não encolhe o desenho.\n    const ler = () => {\n      largura.current = no.clientWidth;\n      setLarguraPlot(no.clientWidth);\n      pintar();\n    };\n    ler();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const obs = new ResizeObserver(ler);\n    obs.observe(no);\n    return () => obs.disconnect();\n  }, [pintar]);\n\n  // Trocar de estilo pelo controle repinta sem esperar dado novo.\n  React.useLayoutEffect(() => {\n    pintar();\n  }, [modoAtual, shape, data.length, pintar]);\n\n  // Dado novo mistura cada camada a partir da forma que está na tela.\n  const morph = React.useRef<{ stop: () => void } | null>(null);\n  React.useEffect(() => {\n    const movs: { key: string; de: number[]; para: number[] }[] = [];\n    for (const [key, para] of alvos) {\n      const de = formas.current.get(key);\n      if (!de || reduzir) {\n        formas.current.set(key, para);\n        continue;\n      }\n      movs.push({ key, de, para });\n    }\n    for (const key of [...formas.current.keys()]) if (!alvos.has(key)) formas.current.delete(key);\n    morph.current?.stop();\n    if (!movs.length) {\n      pintar();\n      return;\n    }\n    morph.current = animate(0, 1, {\n      ...ASSENTA,\n      onUpdate: (t) => {\n        for (const m of movs) formas.current.set(m.key, m.de.map((v, j) => v + (m.para[j]! - v) * t));\n        pintar();\n      },\n      onComplete: () => {\n        for (const m of movs) formas.current.set(m.key, m.para);\n        pintar();\n      },\n    });\n    return () => morph.current?.stop();\n  }, [pintar, reduzir, alvos]);\n\n  // Camada escondida afina até nada enquanto as outras fecham por cima.\n  const chaveEscondidas = escondidas.join(\"|\");\n  React.useEffect(() => {\n    for (const s of series) {\n      const para = escondidas.includes(s.key) ? 0 : 1;\n      const trilha = presenca.current.get(s.key);\n      if (!trilha) {\n        presenca.current.set(s.key, { value: para, stop: null });\n        continue;\n      }\n      if (trilha.value === para) continue;\n      trilha.stop?.();\n      if (reduzir) {\n        trilha.value = para;\n        trilha.stop = null;\n        continue;\n      }\n      const c = animate(trilha.value, para, {\n        ...ASSENTA,\n        onUpdate: (v) => {\n          trilha.value = v;\n          pintar();\n        },\n      });\n      trilha.stop = () => c.stop();\n    }\n    pintar();\n    // A chave junta o conteúdo de `escondidas`; o array em si muda a cada render.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [chaveEscondidas, pintar, reduzir, series.length]);\n\n  // Na primeira vez que aparece, o fluxo incha a partir de um fio no centro.\n  React.useEffect(() => {\n    if (vazio) return;\n    if (reduzir) {\n      cresce.jump(1);\n      pintar();\n      return;\n    }\n    if (!naTela || cresce.get() >= 1) return;\n    const c = animate(cresce, 1, { ...INCHA, onUpdate: pintar });\n    return () => c.stop();\n  }, [vazio, cresce, naTela, pintar, reduzir]);\n\n  // A mira desliza entre pontos; a dica segue numa mola própria e nunca sai do gráfico.\n  const mira = useMotionValue(0);\n  const miraMola = useSpring(mira, SEGUE);\n  const dicaX = useMotionValue(0);\n  const dicaY = useMotionValue(0);\n  const dicaMolaX = useSpring(dicaX, SEGUE);\n  const dicaMolaY = useSpring(dicaY, SEGUE);\n  const estavaAtiva = React.useRef(false);\n  React.useLayoutEffect(() => {\n    const balao = dica.current;\n    const w = largura.current;\n    if (indice === null || !balao || !w) {\n      estavaAtiva.current = false;\n      return;\n    }\n    const x = shape === \"bar\" ? ((indice + 0.5) / data.length) * w : ult > 0 ? (indice / ult) * w : w / 2;\n    let y = hover?.y ?? null;\n    const q = quadro.current;\n    if (y === null && q && ativa) {\n      const i = series.findIndex((s) => s.key === ativa);\n      const j = ult > 0 ? Math.round((indice / ult) * (N - 1)) : 0;\n      if (i >= 0) y = q.toY(shape === \"line\" ? q.upper[i]![j]! : (q.upper[i]![j]! + q.lower[i]![j]!) / 2);\n    }\n    const tw = balao.offsetWidth;\n    const th = balao.offsetHeight;\n    let esq = x + VAO;\n    if (esq + tw > w) esq = x - VAO - tw;\n    const topo = limitar((y ?? height / 2) - th / 2, 0, Math.max(0, height - th));\n    const seco = !estavaAtiva.current || reduzir;\n    mira.set(x);\n    dicaX.set(limitar(esq, 0, Math.max(0, w - tw)));\n    dicaY.set(topo);\n    if (seco) {\n      miraMola.jump(x);\n      dicaMolaX.jump(dicaX.get());\n      dicaMolaY.jump(topo);\n    }\n    estavaAtiva.current = true;\n  });\n\n  // A camada sob o ponteiro, procurada no quadro que está na tela (nota 5).\n  const ler = (clientX: number, clientY: number): Hover | null => {\n    const no = plot.current;\n    const r = no?.getBoundingClientRect();\n    const q = quadro.current;\n    if (!no || !r?.width || vazio) return null;\n    const fracao = limitar((clientX - r.left) / r.width, 0, 1);\n    const escala = r.height / (no.offsetHeight || r.height);\n    const y = (clientY - r.top) / escala;\n    const at =\n      shape === \"bar\" ? limitar(Math.floor(fracao * data.length), 0, ult) : ult > 0 ? Math.round(fracao * ult) : 0;\n    const j = shape === \"bar\" ? (ult > 0 ? Math.round((at / ult) * (N - 1)) : 0) : Math.round(fracao * (N - 1));\n    let key: string | null = null;\n    let maisPerto = 14;\n    if (q)\n      series.forEach((s, i) => {\n        if (escondidas.includes(s.key)) return;\n        const topo = q.toY(q.upper[i]![j]!);\n        const fundo = q.toY(q.lower[i]![j]!);\n        if (shape === \"line\") {\n          // Linha é fina demais pra acertar: vale a mais próxima do ponteiro.\n          if (Math.abs(y - topo) < maisPerto) {\n            maisPerto = Math.abs(y - topo);\n            key = s.key;\n          }\n        } else if (y >= topo - 1 && y <= fundo + 1) key = s.key;\n      });\n    return { index: at, key, y };\n  };\n\n  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.pointerType === \"mouse\" || e.buttons) setHover(ler(e.clientX, e.clientY));\n  };\n  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.pointerType !== \"mouse\") e.currentTarget.setPointerCapture?.(e.pointerId);\n    setHover(ler(e.clientX, e.clientY));\n  };\n  const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    if (vazio) return;\n    if (e.key === \"Escape\" && hover) {\n      e.preventDefault();\n      setHover(null);\n      return;\n    }\n    const de = indice ?? ult;\n    const chaves = visiveis.map((s) => s.key);\n    const at = ativa ? chaves.indexOf(ativa) : -1;\n    const pagina = Math.max(1, Math.round(data.length / 6));\n    const movs: Record<string, () => Hover> = {\n      ArrowLeft: () => ({ index: limitar(de - 1, 0, ult), key: ativa, y: null }),\n      ArrowRight: () => ({ index: limitar(indice === null ? ult : de + 1, 0, ult), key: ativa, y: null }),\n      PageDown: () => ({ index: limitar(de - pagina, 0, ult), key: ativa, y: null }),\n      PageUp: () => ({ index: limitar(de + pagina, 0, ult), key: ativa, y: null }),\n      Home: () => ({ index: 0, key: ativa, y: null }),\n      End: () => ({ index: ult, key: ativa, y: null }),\n      ArrowUp: () => ({\n        index: de,\n        key: chaves[at < 0 ? chaves.length - 1 : Math.min(chaves.length - 1, at + 1)] ?? null,\n        y: null,\n      }),\n      ArrowDown: () => ({ index: de, key: chaves[at < 0 ? 0 : Math.max(0, at - 1)] ?? null, y: null }),\n    };\n    const mov = movs[e.key];\n    if (!mov) return;\n    e.preventDefault();\n    setHover(mov());\n  };\n\n  const formatar = (v: number, s: StreamgraphSeries) =>\n    `${formatValue ? formatValue(v, s) : agrupado.format(v)}${unit ? ` ${unit}` : \"\"}`;\n  const leitura = indice === null ? null : data[indice];\n  const total = (d: StreamgraphDatum) => visiveis.reduce((s, l) => s + Math.max(0, d.values[l.key] ?? 0), 0);\n  const serieAtiva = ativa ? series.find((s) => s.key === ativa) : undefined;\n  const anuncio = leitura\n    ? `${leitura.label}. ${serieAtiva ? `${serieAtiva.label} ${formatar(leitura.values[serieAtiva.key] ?? 0, serieAtiva)}, ` : \"\"}${visiveis.length} camadas, ${agrupado.format(total(leitura))}${unit ? ` ${unit}` : \"\"} no total`\n    : \"\";\n  const resumo = vazio\n    ? `${label}. ${emptyLabel}.`\n    : `${label}, de ${data[0]!.label} a ${data[ult]!.label}. ${visiveis\n        .map((s) => `${s.label}, último ${formatar(data[ult]!.values[s.key] ?? 0, s)}`)\n        .join(\". \")}.`;\n  const marcas = rotulosDoEixo(data, larguraPlot);\n  const ligado = indice !== null;\n\n  return (\n    <figure ref={figura} aria-label={label} className={cn(\"m-0 grid min-w-0 gap-4 pointer-coarse:gap-[max(1rem,calc(1.375rem/var(--alvo-escala,1)-1rem))] text-foreground\", className)}>\n      {legend && series.length > 1 && (\n        <div\n          className=\"-mx-2 flex min-w-0 flex-wrap gap-x-1 gap-y-0.5 pointer-coarse:gap-y-3\"\n          role=\"group\"\n          aria-label={`Camadas de ${label}`}\n          onPointerLeave={() => setChaveLegenda(null)}\n        >\n          {series.map((s, i) => {\n            const ligada = !escondidas.includes(s.key);\n            const emFoco = ativa === s.key;\n            const apagada = ativa !== null && !emFoco;\n            return (\n              <button\n                key={s.key}\n                type=\"button\"\n                aria-pressed={ligada}\n                onClick={() => alternar(s.key)}\n                onPointerEnter={(e) => {\n                  if (e.pointerType === \"mouse\") setChaveLegenda(s.key);\n                }}\n                onFocus={(e) => {\n                  if (e.currentTarget.matches(\":focus-visible\")) setChaveLegenda(s.key);\n                }}\n                onBlur={() => setChaveLegenda(null)}\n                className={cn(\n                  \"relative inline-flex min-h-8 pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:inset-y-[calc((100%-2.75rem/var(--alvo-escala,1))/2)] cursor-pointer items-center gap-2 rounded-[var(--r-pill)] border-0 py-0 pl-2 pr-2.5 text-corpo [-webkit-tap-highlight-color:transparent] [transition:color_var(--dur-fast)_var(--ease-out),background-color_var(--dur-fast)_var(--ease-out),opacity_var(--dur-fast)_var(--ease-out)] hover:text-foreground focus-visible:outline-none focus-visible:[box-shadow:var(--focus)]\",\n                  emFoco ? \"bg-foreground/5 text-foreground\" : \"bg-transparent text-muted-foreground\",\n                  apagada && \"opacity-55\",\n                )}\n              >\n                <span\n                  aria-hidden=\"true\"\n                  className={cn(\n                    \"size-2.5 flex-none rounded-[3px] [transition:scale_var(--dur-base)_var(--ease-out),opacity_var(--dur-fast)_var(--ease-out)]\",\n                    !ligada && \"scale-60 opacity-35\",\n                  )}\n                  style={{ background: corDe(s, i, shape === \"line\") }}\n                />\n                <span className={cn(\"whitespace-nowrap\", !ligada && \"line-through opacity-70\")}>{s.label}</span>\n              </button>\n            );\n          })}\n        </div>\n      )}\n\n      <div className=\"relative grid min-w-0 grid-rows-[auto_26px]\">\n        <div\n          ref={plot}\n          role=\"group\"\n          tabIndex={vazio ? -1 : 0}\n          aria-roledescription=\"gráfico\"\n          aria-label={`${label}. Setas pros lados andam no tempo; pra cima e pra baixo trocam de camada.`}\n          style={{ height }}\n          onPointerMove={onPointerMove}\n          onPointerDown={onPointerDown}\n          onPointerUp={(e) => {\n            if (e.pointerType !== \"mouse\") setHover(null);\n          }}\n          onPointerCancel={() => setHover(null)}\n          onPointerLeave={(e) => {\n            if (e.pointerType === \"mouse\") setHover(null);\n          }}\n          onKeyDown={onKeyDown}\n          onBlur={() => setHover(null)}\n          onFocus={(e) => {\n            if (e.currentTarget.matches(\":focus-visible\") && !vazio)\n              setHover((h) => h ?? { index: ult, key: null, y: null });\n          }}\n          className=\"relative min-w-0 cursor-crosshair touch-pan-y select-none rounded-[var(--r-control)] [-webkit-tap-highlight-color:transparent] focus-visible:outline-none focus-visible:[box-shadow:var(--focus)]\"\n        >\n          <svg className=\"block overflow-visible\" width=\"100%\" height={height} aria-hidden=\"true\" focusable=\"false\">\n            {series.map((s, i) => {\n              const emFoco = ativa === s.key;\n              return (\n                <path\n                  key={s.key}\n                  ref={(no) => {\n                    if (no) caminhos.current.set(s.key, no);\n                    else caminhos.current.delete(s.key);\n                  }}\n                  // O traço da cor do card é o vão entre camadas vizinhas.\n                  className={cn(\n                    \"[stroke-linejoin:round] [stroke-width:2] [stroke:var(--card)] [transition:opacity_var(--dur-fast)_var(--ease-out),fill_var(--dur-base)_var(--ease-out)]\",\n                    ativa !== null && !emFoco && \"opacity-35\",\n                  )}\n                  style={\n                    shape === \"line\"\n                      ? { fill: \"none\", stroke: corDe(s, i, true) }\n                      : { fill: emFoco ? corDe(s, i) : `color-mix(in oklab, ${corDe(s, i)} 82%, var(--card))` }\n                  }\n                />\n              );\n            })}\n            <motion.line\n              x1={0}\n              x2={0}\n              y1={0}\n              y2={height}\n              className={cn(\n                \"pointer-events-none [shape-rendering:crispEdges] [stroke-opacity:0.5] [stroke-width:1] [stroke:var(--foreground)] [transition:opacity_var(--dur-press)_var(--ease-out)]\",\n                ligado ? \"opacity-100\" : \"opacity-0\",\n              )}\n              style={{ x: reduzir ? mira : miraMola }}\n            />\n            {directLabels &&\n              shape === \"area\" &&\n              series.map((s) => (\n                <text\n                  key={s.key}\n                  ref={(no) => {\n                    if (no) rotulos.current.set(s.key, no);\n                    else rotulos.current.delete(s.key);\n                  }}\n                  textAnchor=\"middle\"\n                  dominantBaseline=\"central\"\n                  // Halo da cor do card: o nome lê em cima de qualquer camada.\n                  className={cn(\n                    \"pointer-events-none text-apoio font-medium [fill:var(--foreground)] [paint-order:stroke] [stroke-linejoin:round] [stroke-opacity:0.85] [stroke-width:3px] [stroke:var(--card)] [transition:opacity_var(--dur-slow)_var(--ease-out)]\",\n                    (escondidas.includes(s.key) || (ativa !== null && ativa !== s.key)) && \"!opacity-0\",\n                  )}\n                >\n                  {s.label}\n                </text>\n              ))}\n          </svg>\n\n          <motion.div\n            ref={dica}\n            aria-hidden=\"true\"\n            style={{ x: reduzir ? dicaX : dicaMolaX, y: reduzir ? dicaY : dicaMolaY }}\n            className={cn(\n              \"pointer-events-none absolute left-0 top-0 z-10 grid min-w-[156px] max-w-[min(256px,100%)] gap-1 rounded-[var(--r-surface)] border border-border bg-card px-3 py-2.5 [box-shadow:var(--e-raised)] [transition:opacity_var(--dur-fast)_var(--ease-out),scale_var(--dur-fast)_var(--ease-out)]\",\n              ligado ? \"scale-100 opacity-100\" : \"scale-96 opacity-0\",\n            )}\n          >\n            <p className=\"m-0 mb-0.5 whitespace-nowrap text-apoio tabular-nums text-muted-foreground\">{leitura?.label ?? \"\"}</p>\n            {ordemDaPilha(series.length, modoAtual)\n              .reverse()\n              .map((at) => series[at]!)\n              .filter((s) => !escondidas.includes(s.key))\n              .map((s) => {\n                const emFoco = ativa === s.key;\n                return (\n                  <p\n                    key={s.key}\n                    className={cn(\n                      \"m-0 flex min-w-0 items-center gap-2 text-corpo\",\n                      transicaoRapida,\n                      ativa !== null && !emFoco && \"opacity-50\",\n                    )}\n                  >\n                    <span className=\"h-[2.5px] w-2.5 flex-none rounded-sm\" style={{ background: corDe(s, series.indexOf(s), shape === \"line\") }} />\n                    <span className={cn(\"min-w-0 flex-1 truncate\", emFoco ? \"text-foreground\" : \"text-muted-foreground\")}>\n                      {s.label}\n                    </span>\n                    <span className=\"ml-2 flex-none whitespace-nowrap font-medium tabular-nums text-foreground\">\n                      {leitura ? formatar(leitura.values[s.key] ?? 0, s) : \"\"}\n                    </span>\n                  </p>\n                );\n              })}\n          </motion.div>\n\n          {(vazio || visiveis.length === 0) && (\n            <p className=\"pointer-events-none absolute inset-0 m-0 grid place-items-center text-corpo text-muted-foreground\">\n              {vazio ? emptyLabel : \"Ligue uma camada pra ver o fluxo\"}\n            </p>\n          )}\n        </div>\n\n        <div className=\"relative min-w-0\" aria-hidden=\"true\">\n          {marcas.map((at) => {\n            const fracao = shape === \"bar\" ? (at + 0.5) / data.length : ult > 0 ? at / ult : 0.5;\n            const d = data[at]!;\n            return (\n              <span\n                key={d.key}\n                className=\"absolute top-2 whitespace-nowrap text-apoio leading-none tabular-nums text-muted-foreground [transition:opacity_var(--dur-slow)_var(--ease-out)] starting:opacity-0\"\n                style={{ left: `${fracao * 100}%`, translate: `${-fracao * 100}% 0` }}\n              >\n                {d.axisLabel}\n              </span>\n            );\n          })}\n        </div>\n      </div>\n\n      <p className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n        {anuncio}\n      </p>\n      <p className=\"sr-only\">{resumo}</p>\n      {!vazio && (\n        <div className=\"sr-only\">\n          <table>\n            <caption>{label}</caption>\n            <thead>\n              <tr>\n                <th scope=\"col\">{categoryLabel}</th>\n                {series.map((s) => (\n                  <th key={s.key} scope=\"col\">\n                    {s.label}\n                  </th>\n                ))}\n              </tr>\n            </thead>\n            <tbody>\n              {data.map((d) => (\n                <tr key={d.key}>\n                  <th scope=\"row\">{d.label}</th>\n                  {series.map((s) => (\n                    <td key={s.key}>{formatar(d.values[s.key] ?? 0, s)}</td>\n                  ))}\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n      )}\n    </figure>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Mostrar como a mistura de várias categorias muda no tempo, em dashboard ou relatório",
      "evite": "Ritmo diário de uma atividade: activity-heatmap; valor único de cota, gauge"
    }
  },
  "categories": [
    "conteudo",
    "grafico",
    "dados",
    "serie-temporal",
    "camadas",
    "dashboard"
  ],
  "type": "registry:ui"
}