{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gauge",
  "title": "Gauge",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Medidor em arco com ponteiro que enche quando entra em vista e troca o nome da faixa na hora em que a contagem cruza o limite.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "@visant/motion"
  ],
  "files": [
    {
      "path": "registry/visant/ui/gauge.tsx",
      "content": "\"use client\";\n\n/* Adaptado de Gauge — https://uiarc.dev/components/gauge — Copyright (c) 2026 Elia Kuratli, licença MIT (texto integral em ATTRIBUTIONS.md do @visant/ui). */\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useInView,\n  useMotionValue,\n  useMotionValueEvent,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\n\nimport { DUR, EASE_OUT } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Gauge — um valor contra uma faixa conhecida, com o estado escrito por extenso.\n *\n * Serve pra cota de disco, saúde de conta, ocupação de agenda: um número que só\n * faz sentido contra o teto. Não é barra de progresso de tarefa (isso tem fim) e\n * não é painel de várias metas (isso pede vários medidores ou uma tabela).\n *\n * ── As decisões que não são óbvias ─────────────────────────────────────────\n *\n * 1. **O arco é a marca, o alerta é a exceção.** O traço sai em `var(--brand)`;\n *    só a faixa marcada com `alert` troca pra `--destructive`. Cor nunca carrega\n *    o estado sozinha: o nome da faixa (\"Quase cheio\") aparece escrito embaixo\n *    do número, em cor de texto, com um ponto da cor do arco ao lado.\n * 2. **O ponteiro cruza o anel.** Um risco curto em `--foreground` atravessa o\n *    traço no ponto do valor. É o que deixa ler o medidor quando a marca do\n *    cliente é clara demais pra contrastar com a trilha.\n * 3. **O estado troca quando a contagem cruza o limite, não antes.** A faixa\n *    acompanha o número que está na tela durante o enchimento, então a cor e o\n *    nome mudam exatamente na passagem. Mudar tudo no primeiro quadro entregaria\n *    o desfecho antes do gesto.\n * 4. **Enche uma vez, quando metade dele entra em vista**, e depois só reage a\n *    valor novo. Com `prefers-reduced-motion` o valor chega pronto.\n * 5. **Os dígitos ficam alinhados à direita numa caixa do tamanho do valor\n *    final**, pra coluna das unidades não pular enquanto conta.\n */\n\nexport interface GaugeBand {\n  /** Valor (na escala de `min`..`max`) onde a faixa começa. */\n  from: number;\n  /** Nome do estado, escrito na tela. */\n  label: string;\n  /** Faixa de alerta: o arco troca pra `--destructive`. */\n  alert?: boolean;\n}\n\nexport interface GaugeProps extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n  /** Valor atual, entre `min` e `max`. */\n  value: number;\n  min?: number;\n  max?: number;\n  /** O que está sendo medido. Vira o nome acessível. */\n  label: string;\n  /** Qualificador: a régua em palavras (\"360 de 500 GB\"). */\n  detail?: string;\n  /** Faixas por valor inicial. A maior faixa alcançada dá o nome e a cor. */\n  bands?: GaugeBand[];\n  /** Largura máxima do anel, em px. */\n  size?: number;\n}\n\n// Anel de 270° aberto embaixo, desenhado no sentido horário a partir da ponta esquerda.\nconst RAIO = 42;\nconst INICIO = -135;\nconst VAO = 270;\n\nconst ponto = (angulo: number, r: number) => {\n  const rad = (angulo * Math.PI) / 180;\n  return [50 + r * Math.sin(rad), 50 - r * Math.cos(rad)] as const;\n};\n\nfunction arco(fracao: number) {\n  const fim = INICIO + VAO * Math.min(1, Math.max(0, fracao));\n  const [x0, y0] = ponto(INICIO, RAIO);\n  const [x1, y1] = ponto(fim, RAIO);\n  return `M ${x0.toFixed(3)} ${y0.toFixed(3)} A ${RAIO} ${RAIO} 0 ${fim - INICIO > 180 ? 1 : 0} 1 ${x1.toFixed(3)} ${y1.toFixed(3)}`;\n}\n\nconst TRILHA = arco(1);\n// Primeiro enchimento: mais lento e sem passar do alvo, como um anel que fecha.\nconst ENCHER = { type: \"spring\", visualDuration: 0.8, bounce: 0 } as const;\n// Valor novo depois disso: mola curta, com um fio de vida.\nconst MUDAR = { type: \"spring\", visualDuration: 0.4, bounce: 0.12 } as const;\n\nexport function Gauge({\n  value,\n  min = 0,\n  max = 100,\n  label,\n  detail,\n  bands,\n  size = 176,\n  className,\n  ...props\n}: GaugeProps) {\n  const teto = max > min ? max : min + 1;\n  const fracao = Math.min(1, Math.max(0, (value - min) / (teto - min)));\n  const pct = Math.round(fracao * 100);\n\n  const faixas = React.useMemo(() => [...(bands ?? [])].sort((a, b) => a.from - b.from), [bands]);\n  const faixaEm = React.useCallback(\n    (v: number) => faixas.filter((f) => v >= f.from - 1e-6).pop(),\n    [faixas],\n  );\n  const faixaFinal = faixaEm(value);\n\n  const ref = React.useRef<HTMLElement>(null);\n  const emVista = useInView(ref, { once: true, amount: 0.5 });\n  const reduzir = !!useReducedMotion();\n\n  const varre = useMotionValue(0);\n  const conta = useMotionValue(0);\n  const [cheio, setCheio] = React.useState(false);\n  const [faixaViva, setFaixaViva] = React.useState(faixaFinal);\n\n  const caminho = useTransform(varre, arco);\n  // A ponta redonda some no primeiro fiapo em vez de aparecer como um pingo.\n  const opacidadeArco = useTransform(varre, (v) => Math.min(1, v / 0.02));\n  const ponteiro = useTransform(varre, (v) => {\n    const ang = INICIO + VAO * Math.min(1, Math.max(0, v));\n    const [xa, ya] = ponto(ang, 34);\n    const [xb, yb] = ponto(ang, 50.5);\n    return `M ${xa.toFixed(3)} ${ya.toFixed(3)} L ${xb.toFixed(3)} ${yb.toFixed(3)}`;\n  });\n  const digitos = useTransform(conta, (v) => String(Math.round(Math.min(100, Math.max(0, v)))));\n\n  React.useEffect(() => {\n    if (reduzir) {\n      varre.jump(fracao);\n      conta.jump(fracao * 100);\n      setFaixaViva(faixaFinal);\n      setCheio(true);\n      return;\n    }\n    if (!emVista) return;\n    const transicao = cheio ? MUDAR : ENCHER;\n    const a = animate(varre, fracao, transicao);\n    const b = animate(conta, fracao * 100, {\n      ...(cheio ? { type: \"spring\", visualDuration: 0.4, bounce: 0 } : ENCHER),\n      onComplete: () => setCheio(true),\n    });\n    return () => {\n      a.stop();\n      b.stop();\n    };\n  }, [emVista, fracao, reduzir, cheio, varre, conta, faixaFinal]);\n\n  // A faixa segue o número na tela: cor e nome trocam quando a contagem cruza o limite.\n  useMotionValueEvent(conta, \"change\", (v) => {\n    const proxima = faixaEm(min + (v / 100) * (teto - min));\n    if (proxima !== faixaViva) setFaixaViva(proxima);\n  });\n\n  const faixa = reduzir ? faixaFinal : faixaViva;\n  const cor = faixa?.alert ? \"var(--destructive)\" : \"var(--brand)\";\n  const valorAria = Math.min(teto, Math.max(min, value));\n\n  return (\n    <figure\n      ref={ref}\n      className={cn(\"m-0 grid min-w-0 justify-items-center gap-2.5 text-center\", className)}\n      style={{ [\"--gauge-cor\" as string]: cor }}\n      {...props}\n    >\n      <div\n        role=\"meter\"\n        aria-label={label}\n        aria-valuemin={min}\n        aria-valuemax={teto}\n        aria-valuenow={valorAria}\n        aria-valuetext={`${pct}%${faixaFinal ? `, ${faixaFinal.label}` : \"\"}${detail ? `, ${detail}` : \"\"}`}\n        className=\"relative aspect-[100/90] w-full\"\n        style={{ maxWidth: size }}\n      >\n        <svg viewBox=\"0 0 100 90\" aria-hidden=\"true\" focusable=\"false\" className=\"block size-full overflow-visible\">\n          <path d={TRILHA} fill=\"none\" strokeWidth={9} strokeLinecap=\"round\" className=\"stroke-muted\" />\n          <motion.path\n            d={caminho}\n            fill=\"none\"\n            strokeWidth={9}\n            strokeLinecap=\"round\"\n            style={{ opacity: opacidadeArco, stroke: \"var(--gauge-cor)\" }}\n            className=\"[transition:stroke_var(--dur-base)_var(--ease-out)] motion-reduce:[transition:none]\"\n          />\n          {/* O ponteiro: um risco radial que atravessa o anel no ponto do valor. */}\n          <motion.path d={ponteiro} fill=\"none\" strokeWidth={2.5} strokeLinecap=\"round\" className=\"stroke-foreground\" />\n        </svg>\n\n        {/* O centro de verdade do anel é o meio de um quadrado da largura dele. */}\n        <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-x-0 top-0 grid aspect-square place-content-center justify-items-center gap-1\">\n          <span className=\"relative inline-flex items-baseline font-semibold leading-none tracking-tight tabular-nums text-foreground text-[2rem]\">\n            <span className=\"relative inline-flex justify-end\">\n              {/* Reserva a largura do valor final; os dígitos vivos se alinham à direita dela. */}\n              <span className=\"invisible\">{pct}</span>\n              <motion.span className=\"absolute inset-y-0 right-0 whitespace-nowrap\">{digitos}</motion.span>\n            </span>\n            <span className=\"ml-px text-[0.5em] font-medium text-muted-foreground\">%</span>\n          </span>\n          {faixas.length > 0 && (\n            <span className=\"relative inline-flex h-[1.45em] min-w-24 items-center justify-center overflow-hidden text-apoio font-medium text-foreground\">\n              <AnimatePresence mode=\"popLayout\" initial={false}>\n                <motion.span\n                  key={(cheio || reduzir) && faixa ? faixa.label : \"\"}\n                  className=\"inline-flex items-center gap-1.5 whitespace-nowrap\"\n                  initial={reduzir ? { opacity: 0 } : { opacity: 0, y: \"0.4em\" }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={reduzir ? { opacity: 0 } : { opacity: 0, y: \"-0.4em\" }}\n                  transition={{ duration: DUR.base, ease: EASE_OUT }}\n                >\n                  {(cheio || reduzir) && faixa && (\n                    <>\n                      <span className=\"size-1.5 rounded-[var(--r-pill)]\" style={{ background: \"var(--gauge-cor)\" }} />\n                      {faixa.label}\n                    </>\n                  )}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          )}\n        </div>\n      </div>\n\n      <figcaption className=\"grid min-w-0 gap-0.5\">\n        <strong className=\"text-corpo font-medium text-foreground\">{label}</strong>\n        {detail && <span className=\"text-apoio tabular-nums text-muted-foreground\">{detail}</span>}\n      </figcaption>\n    </figure>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Medidor de cota ou uso em dashboard, com o nome da faixa trocando ao cruzar o limite",
      "evite": "Métrica com régua e evidência: measure-bar; nível de áudio ao vivo, vintage-meter"
    }
  },
  "categories": [
    "conteudo",
    "dados",
    "medidor",
    "cota",
    "grafico",
    "meter"
  ],
  "type": "registry:ui"
}