{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "magnet",
  "type": "registry:component",
  "title": "Magnet",
  "description": "Wrap any element and it gets pulled toward the cursor with spring physics.",
  "author": "Marcelo Treviño Velázquez <https://furin.dev>",
  "dependencies": [],
  "categories": [
    "Interaction"
  ],
  "docs": "Installed in components/furin/magnet/. Docs, live controls and props table: https://furin.dev/components/magnet/",
  "files": [
    {
      "path": "registry/magnet/Magnet.tsx",
      "content": "\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport \"./Magnet.css\";\n\nexport interface MagnetProps {\n  /** Lo que se deja atraer: un botón, una tarjeta, un ícono, lo que sea. */\n  children?: ReactNode;\n  /** Qué tanto se acerca al cursor: 0 no se mueve, 1 lo sigue exactamente. */\n  strength?: number;\n  /** Distancia en px, medida desde el borde del elemento, a la que empieza la atracción. */\n  radius?: number;\n  /** Rigidez del resorte: más alta, responde más rápido. */\n  stiffness?: number;\n  /** Amortiguación del resorte: más baja, rebota más al regresar. */\n  damping?: number;\n  /** Apaga la atracción (el elemento se queda quieto). */\n  disabled?: boolean;\n  /** Clase CSS extra para el contenedor. */\n  className?: string;\n  /** Estilos en línea del contenedor. */\n  style?: CSSProperties;\n}\n\n/** Estado del resorte: posición, velocidad y punto de reposo, en px. */\ninterface Resorte {\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  objetivoX: number;\n  objetivoY: number;\n}\n\n/** El resorte se integra en pasos de este tamaño (en segundos) para que sea estable. */\nconst PASO_FISICO = 1 / 240;\n/** Si un cuadro tarda más que esto (pestaña dormida), se recorta para no dar un salto. */\nconst DT_MAXIMO = 1 / 30;\n\nexport function Magnet({\n  children,\n  strength = 0.4,\n  radius = 140,\n  stiffness = 170,\n  damping = 11,\n  disabled = false,\n  className = \"\",\n  style,\n}: MagnetProps) {\n  const envoltura = useRef<HTMLDivElement>(null);\n  const interior = useRef<HTMLDivElement>(null);\n  // Los ajustes viven en una ref para poder cambiarlos en vivo sin reiniciar el resorte.\n  const ajustes = useRef({ strength, radius, stiffness, damping });\n\n  useEffect(() => {\n    ajustes.current = { strength, radius, stiffness, damping };\n  }, [strength, radius, stiffness, damping]);\n\n  useEffect(() => {\n    const caja = envoltura.current;\n    const pieza = interior.current;\n    if (!caja || !pieza || disabled) return;\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n\n    const r: Resorte = { x: 0, y: 0, vx: 0, vy: 0, objetivoX: 0, objetivoY: 0 };\n    let cuadro = 0;\n    let ultimoCuadro = 0;\n\n    const dibujar = () => {\n      pieza.style.transform = `translate3d(${r.x.toFixed(2)}px, ${r.y.toFixed(2)}px, 0)`;\n    };\n\n    const paso = (ahora: number) => {\n      const dt = Math.min((ahora - ultimoCuadro) / 1000, DT_MAXIMO);\n      ultimoCuadro = ahora;\n      const { stiffness: k, damping: c } = ajustes.current;\n\n      const pasos = Math.max(1, Math.ceil(dt / PASO_FISICO));\n      const h = dt / pasos;\n      for (let i = 0; i < pasos; i++) {\n        // Resorte amortiguado: la fuerza lo jala al objetivo y la fricción lo frena.\n        r.vx += (-k * (r.x - r.objetivoX) - c * r.vx) * h;\n        r.vy += (-k * (r.y - r.objetivoY) - c * r.vy) * h;\n        r.x += r.vx * h;\n        r.y += r.vy * h;\n      }\n\n      const quieto =\n        Math.abs(r.x - r.objetivoX) < 0.05 &&\n        Math.abs(r.y - r.objetivoY) < 0.05 &&\n        Math.abs(r.vx) < 0.05 &&\n        Math.abs(r.vy) < 0.05;\n\n      if (quieto) {\n        r.x = r.objetivoX;\n        r.y = r.objetivoY;\n        r.vx = 0;\n        r.vy = 0;\n        cuadro = 0; // el bucle se duerme hasta que el cursor vuelva a moverse\n      } else {\n        cuadro = requestAnimationFrame(paso);\n      }\n      dibujar();\n    };\n\n    const despertar = () => {\n      if (cuadro !== 0) return;\n      ultimoCuadro = performance.now();\n      cuadro = requestAnimationFrame(paso);\n    };\n\n    const soltar = () => {\n      r.objetivoX = 0;\n      r.objetivoY = 0;\n      despertar();\n    };\n\n    const alMoverElPuntero = (e: PointerEvent) => {\n      // La envoltura no se mueve (solo su interior), así que su caja es el punto de reposo.\n      const area = caja.getBoundingClientRect();\n      const { strength: fuerza, radius: alcance } = ajustes.current;\n\n      // Distancia del cursor a la caja: 0 si está encima, cuenta desde el borde si está fuera.\n      const dx = Math.max(area.left - e.clientX, 0, e.clientX - area.right);\n      const dy = Math.max(area.top - e.clientY, 0, e.clientY - area.bottom);\n      const distancia = Math.hypot(dx, dy);\n\n      if (distancia > alcance) {\n        soltar();\n        return;\n      }\n\n      // La atracción se desvanece hacia el borde del radio para que no haya tirones al salir.\n      const peso = alcance > 0 ? 1 - (distancia / alcance) ** 2 : 1;\n      const centroX = area.left + area.width / 2;\n      const centroY = area.top + area.height / 2;\n      r.objetivoX = (e.clientX - centroX) * fuerza * peso;\n      r.objetivoY = (e.clientY - centroY) * fuerza * peso;\n      despertar();\n    };\n\n    // En pantallas táctiles no hay cursor flotando: al levantar el dedo se suelta.\n    const alLevantar = (e: PointerEvent) => {\n      if (e.pointerType !== \"mouse\") soltar();\n    };\n\n    window.addEventListener(\"pointerdown\", alMoverElPuntero, { passive: true });\n    window.addEventListener(\"pointermove\", alMoverElPuntero, { passive: true });\n    window.addEventListener(\"pointerup\", alLevantar, { passive: true });\n    window.addEventListener(\"pointercancel\", soltar, { passive: true });\n    document.documentElement.addEventListener(\"pointerleave\", soltar, { passive: true });\n\n    return () => {\n      cancelAnimationFrame(cuadro);\n      window.removeEventListener(\"pointerdown\", alMoverElPuntero);\n      window.removeEventListener(\"pointermove\", alMoverElPuntero);\n      window.removeEventListener(\"pointerup\", alLevantar);\n      window.removeEventListener(\"pointercancel\", soltar);\n      document.documentElement.removeEventListener(\"pointerleave\", soltar);\n      pieza.style.transform = \"\";\n    };\n  }, [disabled]);\n\n  return (\n    <div ref={envoltura} className={`furin-magnet ${className}`.trim()} style={style}>\n      <div ref={interior} className=\"furin-magnet__interior\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport default Magnet;\n",
      "type": "registry:component",
      "target": "components/furin/magnet/Magnet.tsx"
    },
    {
      "path": "registry/magnet/Magnet.css",
      "content": "/* La envoltura se queda quieta (marca el punto de reposo); solo se mueve su interior. */\n.furin-magnet {\n  display: inline-block;\n}\n\n.furin-magnet__interior {\n  display: block;\n  will-change: transform;\n}\n",
      "type": "registry:file",
      "target": "components/furin/magnet/Magnet.css"
    }
  ]
}
