{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eclipse-switch",
  "title": "Eclipse Switch",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Chave de tema claro e escuro em que a lua cobre o sol num eclipse, sem mexer na página por conta própria.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "@visant/motion"
  ],
  "files": [
    {
      "path": "registry/visant/ui/eclipse-switch.tsx",
      "content": "\"use client\";\n\n// Adaptado de Eclipse — https://uiarc.dev/components/theme-switch-eclipse — Copyright (c) 2026 Elia Kuratli, licença MIT (texto integral em ATTRIBUTIONS.md do @visant/ui).\n\nimport * as React from \"react\";\nimport { motion, useReducedMotion, type Transition } from \"motion/react\";\n\nimport { INSTANT } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Eclipse Switch: a chave de claro e escuro que conta a troca como um eclipse.\n *\n * O ícone de sol que vira lua é a chave mais vista da web, e por isso some na\n * página. Aqui a troca é um gesto só: a lua desliza por cima do sol, cobre o\n * disco e deixa um crescente; a coroa em volta encolhe junto. Voltar ao claro é\n * a lua saindo pelo mesmo caminho. Uma peça, uma direção, nada de dois ícones\n * cruzando opacidade.\n *\n * ── Decisões que não são óbvias ────────────────────────────────────────────\n *\n * 1. **A chave não toca na página.** Ela não escreve em `document`, não põe\n *    classe no `<html>` e não lê `localStorage`. Ela só diz qual é o próximo\n *    tema por `onThemeChange` e entrega o botão que disparou, pra quem quiser\n *    começar uma view transition dali. Quem aplica o tema é o app: cada cliente\n *    guarda a preferência de um jeito, e uma chave que decide isso sozinha briga\n *    com o provedor que já existe.\n * 2. **Controlada ou não.** Com `theme` ela obedece; sem, guarda o próprio\n *    estado a partir de `defaultTheme`. Assim ela funciona solta numa vitrine e\n *    presa ao provedor num produto.\n * 3. **A lua é um disco da cor do botão, não um ícone.** O crescente nasce da\n *    sobreposição, então a lua precisa ter EXATAMENTE o fundo da chave. É por\n *    isso que o hover mexe na borda e nunca no fundo: fundo que muda no hover\n *    descobriria a lua como uma mancha.\n * 4. **O primeiro tema não anima.** Uma página que carrega no escuro não deve\n *    ver o eclipse acontecer sozinho; só troca depois da montagem viaja.\n */\n\nexport type Theme = \"light\" | \"dark\";\n\nexport interface EclipseSwitchProps\n  extends Omit<React.ComponentPropsWithoutRef<\"button\">, \"onClick\" | \"children\"> {\n  /** Tema atual. Passe para controlar; omita para a chave guardar o próprio estado. */\n  theme?: Theme;\n  /** Tema inicial quando não controlada. */\n  defaultTheme?: Theme;\n  /** Chamado no clique com o próximo tema e o botão que disparou. Quem aplica o tema é você. */\n  onThemeChange?: (next: Theme, trigger: HTMLButtonElement) => void;\n  /** Nome acessível. Por padrão diz para qual tema vai. */\n  label?: string;\n  /** Só o eclipse, num quadrado. Sem o texto ao lado. */\n  iconOnly?: boolean;\n}\n\n// Mola que assenta sem quicar: o eclipse é um deslize, não um salto.\nconst mola: Transition = { type: \"spring\", visualDuration: 0.3, bounce: 0.12 };\nconst parado: Transition = INSTANT;\nconst RAIOS = [0, 45, 90, 135, 180, 225, 270, 315];\n\nexport function EclipseSwitch({\n  theme,\n  defaultTheme = \"light\",\n  onThemeChange,\n  label,\n  iconOnly = false,\n  className,\n  ...rest\n}: EclipseSwitchProps) {\n  const [interno, setInterno] = React.useState<Theme>(defaultTheme);\n  const atual = theme ?? interno;\n  const escuro = atual === \"dark\";\n  const proximo: Theme = escuro ? \"light\" : \"dark\";\n  const reduce = useReducedMotion() ?? false;\n\n  // Só depois do primeiro quadro as trocas viajam (decisão 4).\n  const [montado, setMontado] = React.useState(false);\n  React.useEffect(() => {\n    const id = requestAnimationFrame(() => setMontado(true));\n    return () => cancelAnimationFrame(id);\n  }, []);\n  const t = reduce || !montado ? parado : mola;\n\n  return (\n    <button\n      type=\"button\"\n      {...rest}\n      aria-pressed={escuro}\n      aria-label={label ?? (escuro ? \"Mudar para o tema claro\" : \"Mudar para o tema escuro\")}\n      data-theme={atual}\n      onClick={(e) => {\n        if (theme === undefined) setInterno(proximo);\n        onThemeChange?.(proximo, e.currentTarget);\n      }}\n      className={cn(\n        \"relative inline-flex select-none items-center justify-center gap-2 border border-border bg-card text-sm font-medium text-foreground pointer-coarse:after:absolute pointer-coarse:after:-inset-1\",\n        \"rounded-[var(--r-control)] outline-none [box-shadow:var(--e-raised)]\",\n        \"transition-[border-color] duration-[var(--dur-fast)] ease-[var(--ease-out)]\",\n        \"hover:border-foreground/30 focus-visible:[box-shadow:var(--focus)]\",\n        iconOnly ? \"size-10\" : \"h-10 px-3.5\",\n        className,\n      )}\n    >\n      <span aria-hidden=\"true\" className=\"relative grid size-5 place-items-center overflow-hidden\">\n        {/*\n          Coroa: oito raios que se recolhem quando o disco é coberto.\n\n          Era um anel em volta do disco, e anel com disco no meio é o desenho\n          de um botão de rádio: no estado claro a chave lia como \"opção\n          marcada\", não como sol. Raio é o que diz sol em qualquer tamanho.\n        */}\n        {RAIOS.map((angulo, i) => (\n          <span\n            key={angulo}\n            className=\"absolute inset-0 grid place-items-center\"\n            style={{ transform: `rotate(${angulo}deg)` }}\n          >\n            <motion.span\n              className=\"block h-[3px] w-[1.5px] -translate-y-[7.5px] rounded-[1px] bg-foreground\"\n              initial={false}\n              animate={escuro ? { scaleY: 0, opacity: 0 } : { scaleY: 1, opacity: 1 }}\n              transition={reduce || !montado ? parado : { ...mola, delay: escuro ? 0 : 0.03 * i }}\n            />\n          </span>\n        ))}\n        {/* Sol: o disco que fica. */}\n        <span className=\"absolute size-[9px] rounded-full bg-foreground\" />\n        {/* Lua: disco da cor do botão que desliza por cima e deixa o crescente. */}\n        <motion.span\n          className=\"absolute rounded-full bg-card\"\n          initial={false}\n          animate={escuro ? { x: 3, y: -3 } : { x: 14, y: -14 }}\n          style={{ width: 9, height: 9 }}\n          transition={t}\n        />\n      </span>\n      {!iconOnly && <span>{escuro ? \"Escuro\" : \"Claro\"}</span>}\n    </button>\n  );\n}\n\nexport default EclipseSwitch;\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Chave de tema claro e escuro no cabeçalho, com a lua cobrindo o sol",
      "evite": "Switch de configuração comum: liquid-toggle; trocar de marca, brand-selector"
    }
  },
  "categories": [
    "controles",
    "tema",
    "modo-escuro",
    "chave",
    "eclipse",
    "toggle"
  ],
  "type": "registry:ui"
}