{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-cursor",
  "title": "Dot Cursor",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Ponto de 6px que segue o ponteiro com mola calma e abre num anel vazado sobre link, botão ou [data-cursor], em mix-blend-difference; só existe em ponteiro fino e sem movimento reduzido.",
  "dependencies": [
    "framer-motion"
  ],
  "registryDependencies": [
    "utils",
    "@visant/motion"
  ],
  "files": [
    {
      "path": "registry/visant/ui/dot-cursor.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useMotionValue, useSpring } from \"framer-motion\";\n\nimport { DUR, EASE_OUT } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Dot Cursor — um ponto de 6px que acompanha o ponteiro com mola calma.\n *\n * Sobre algo interativo (`[data-cursor]`, link, botão) o ponto abre num anel\n * fino e vazado de 28px. Sem rótulo, sem bolha cheia, sem \"View →\" escrito\n * dentro: o anel diz \"isto responde\" e para por aí. O cursor nativo continua\n * lá — o ponto é companhia, não substituto, e por isso nunca atrapalha a mira.\n *\n * Monte UMA vez, perto da raiz do app. Ele é `position: fixed` e ouve a janela.\n *\n * ── As armadilhas ──────────────────────────────────────────────────────────\n *\n * 1. **O `translate` de centralizar mora num wrapper.** O `scale` animado pelo\n *    framer vira `transform` inline e sobrescreveria o `-translate-1/2` do\n *    Tailwind no mesmo nó — o anel cresceria a partir do canto, fora do\n *    ponteiro. Um nó centraliza, o de dentro escala.\n * 2. **Só em ponteiro fino e sem `prefers-reduced-motion`.** No toque não há\n *    ponteiro para seguir (o ponto ficaria preso no último toque), e um objeto\n *    perseguindo o mouse é exatamente o movimento que a preferência pede pra\n *    tirar. A guarda é checada ANTES de assinar o listener.\n * 3. **O preenchimento é transição CSS, não framer.** `currentColor` não é um\n *    valor que o framer saiba interpolar — ele salta. O CSS interpola\n *    `transparent ↔ currentColor` sem esforço, então o framer cuida só do\n *    `scale` e o fundo fica com `transition-[background-color]`.\n * 4. **`mix-blend-mode: difference` precisa de tinta CLARA.** É o que faz o\n *    ponto inverter sobre foto, texto e fundo e aparecer em qualquer um deles.\n *    O padrão é `var(--background)`, que é claro num tema claro; num tema\n *    escuro passe `ink=\"var(--foreground)\"` — tinta escura em `difference`\n *    não inverte nada e o ponto some.\n * 5. **Saiu da janela, o ponto apaga.** Sem o `pointerleave` no documento ele\n *    ficaria parado na borda por onde o mouse saiu.\n */\n\n// Mola sem overshoot: segue, não quica. Um cursor que passa do ponto parece bêbado.\nconst SPRING_FOLLOW = { stiffness: 160, damping: 22, mass: 0.6 } as const;\n\nconst ANEL = 28; // px — o anel aberto\nconst PONTO = 6; // px — o ponto em repouso\n\nexport interface DotCursorProps {\n  /** O que conta como interativo. O anel abre sobre o mais próximo deles. */\n  selector?: string;\n  /** Tinta do ponto. Precisa ser clara com `blend` ligado (armadilha 4). */\n  ink?: string;\n  /** `mix-blend-mode: difference`. Desligue para um ponto de cor fixa. */\n  blend?: boolean;\n  className?: string;\n}\n\nexport function DotCursor({\n  selector = \"[data-cursor], a, button\",\n  ink = \"var(--background)\",\n  blend = true,\n  className,\n}: DotCursorProps) {\n  const [ligado, setLigado] = React.useState(false);\n  const [ativo, setAtivo] = React.useState(false);\n  const [sobre, setSobre] = React.useState(false);\n  // Nasce fora da tela: o primeiro quadro não pode piscar um ponto no canto.\n  const x = useMotionValue(-100);\n  const y = useMotionValue(-100);\n  const sx = useSpring(x, SPRING_FOLLOW);\n  const sy = useSpring(y, SPRING_FOLLOW);\n\n  React.useEffect(() => {\n    const fino = window.matchMedia(\"(pointer: fine)\").matches;\n    const reduz = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    if (!fino || reduz) return;\n    setLigado(true);\n\n    const mover = (e: PointerEvent) => {\n      x.set(e.clientX);\n      y.set(e.clientY);\n      // `setState` com o mesmo valor não re-renderiza; checar a cada movimento\n      // é barato e pega o caso do conteúdo que mudou sob o ponteiro parado.\n      setSobre(!!(e.target as Element | null)?.closest?.(selector));\n      setAtivo(true);\n    };\n    const sair = () => setAtivo(false);\n    window.addEventListener(\"pointermove\", mover, { passive: true });\n    document.documentElement.addEventListener(\"pointerleave\", sair);\n    return () => {\n      window.removeEventListener(\"pointermove\", mover);\n      document.documentElement.removeEventListener(\"pointerleave\", sair);\n    };\n  }, [x, y, selector]);\n\n  // Sem ponteiro fino não há nem nó: nada fixo flutuando no toque.\n  if (!ligado) return null;\n\n  return (\n    <motion.div\n      aria-hidden\n      className={cn(\n        \"pointer-events-none fixed left-0 top-0 z-[60]\",\n        blend && \"mix-blend-difference\",\n        className,\n      )}\n      style={{ x: sx, y: sy, color: ink }}\n      initial={false}\n      animate={{ opacity: ativo ? 1 : 0 }}\n      transition={{ duration: DUR.base, ease: EASE_OUT }}\n    >\n      {/* translate num wrapper: o scale do framer sobrescreveria o transform do Tailwind */}\n      <div className=\"-translate-x-1/2 -translate-y-1/2\">\n        <motion.div\n          data-over={sobre ? \"\" : undefined}\n          className=\"rounded-full border border-current bg-current transition-[background-color] [transition-duration:var(--dur-slow,300ms)] [transition-timing-function:var(--ease-out)] data-[over]:bg-transparent\"\n          style={{ width: ANEL, height: ANEL }}\n          initial={false}\n          animate={{ scale: sobre ? 1 : PONTO / ANEL }}\n          transition={{ duration: DUR.slow, ease: EASE_OUT }}\n        />\n      </div>\n    </motion.div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Cursor customizado mínimo em site editorial ou portfólio, abrindo anel sobre links",
      "evite": "App de produto ou formulário: cursor do sistema; efeito de calor sob o cursor, heat-word"
    }
  },
  "categories": [
    "efeitos",
    "cursor",
    "ponteiro",
    "hover",
    "mola",
    "blend",
    "minimal"
  ],
  "type": "registry:ui"
}