{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "magnetic-button",
  "title": "Magnetic Button",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The Ballmac Button with a magnetic pull: it drifts toward a nearby mouse pointer on a spring, and its label moves a little further for depth. Takes every Button prop.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/motion-presets.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/magnetic-button.tsx",
      "content": "// Ballmac UI: Magnetic Button. https://ui.ballmac.com/components/magnetic-button\n\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from \"motion/react\"\n\nimport { Button, type ButtonProps } from \"@/components/ballmac/button\"\nimport { cn } from \"@/lib/utils\"\nimport { spring } from \"@/lib/ballmac/motion\"\n\ntype MagneticButtonProps = Omit<ButtonProps, \"asChild\"> & {\n  /** How far the button follows the pointer, as a fraction of the pointer's offset from its center. */\n  strength?: number\n  /** Distance in pixels outside the button's edge where the pull starts. */\n  radius?: number\n  /** Classes for the wrapper that holds the button's place in the layout. */\n  wrapperClassName?: string\n}\n\n/** The label travels this much further than the button, which reads as depth. */\nconst LABEL_DEPTH = 0.35\n\nfunction MagneticButton({\n  strength = 0.2,\n  radius = 72,\n  wrapperClassName,\n  disabled,\n  loading,\n  children,\n  ...props\n}: MagneticButtonProps) {\n  const anchorRef = React.useRef<HTMLSpanElement>(null)\n  const reduceMotion = useReducedMotion()\n  const x = useMotionValue(0)\n  const y = useMotionValue(0)\n  const springX = useSpring(x, spring.snappy)\n  const springY = useSpring(y, spring.snappy)\n  const labelX = useTransform(springX, (v) => v * LABEL_DEPTH)\n  const labelY = useTransform(springY, (v) => v * LABEL_DEPTH)\n  const inactive = !!reduceMotion || !!disabled || !!loading\n\n  React.useEffect(() => {\n    if (inactive) {\n      x.set(0)\n      y.set(0)\n      return\n    }\n    const anchor = anchorRef.current\n    if (!anchor) return\n    const onMove = (event: PointerEvent) => {\n      if (event.pointerType !== \"mouse\") return\n      // The anchor is never transformed, so its box is the button's resting position.\n      const rect = anchor.getBoundingClientRect()\n      const dx = event.clientX - (rect.left + rect.width / 2)\n      const dy = event.clientY - (rect.top + rect.height / 2)\n      // Distance from the button's edge (0 when the pointer is over it).\n      const outsideX = Math.max(0, Math.abs(dx) - rect.width / 2)\n      const outsideY = Math.max(0, Math.abs(dy) - rect.height / 2)\n      const distance = Math.hypot(outsideX, outsideY)\n      if (distance > radius) {\n        x.set(0)\n        y.set(0)\n        return\n      }\n      const falloff = radius > 0 ? 1 - distance / radius : 1\n      x.set(dx * strength * falloff)\n      y.set(dy * strength * falloff)\n    }\n    const reset = () => {\n      x.set(0)\n      y.set(0)\n    }\n    // Pointer left the window: relatedTarget is null.\n    const onOut = (event: PointerEvent) => {\n      if (!event.relatedTarget) reset()\n    }\n    window.addEventListener(\"pointermove\", onMove, { passive: true })\n    document.addEventListener(\"pointerout\", onOut)\n    window.addEventListener(\"blur\", reset)\n    return () => {\n      window.removeEventListener(\"pointermove\", onMove)\n      document.removeEventListener(\"pointerout\", onOut)\n      window.removeEventListener(\"blur\", reset)\n    }\n  }, [inactive, radius, strength, x, y])\n\n  return (\n    <span ref={anchorRef} data-slot=\"magnetic-button\" className={cn(\"inline-flex\", wrapperClassName)}>\n      <motion.span className=\"inline-flex\" style={{ x: springX, y: springY }}>\n        <Button disabled={disabled} loading={loading} {...props}>\n          <motion.span data-slot=\"magnetic-button-label\" className=\"inline-flex items-center gap-[inherit]\" style={{ x: labelX, y: labelY }}>\n            {children}\n          </motion.span>\n        </Button>\n      </motion.span>\n    </span>\n  )\n}\n\nexport { MagneticButton, type MagneticButtonProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/magnetic-button.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "button",
      "cta",
      "hover",
      "spring",
      "pointer",
      "motion"
    ],
    "version": "1.0.0",
    "updated": "2026-09-28",
    "ai": {
      "summary": "Use like Button (variant, size, shape, loading, onClick…). The pull only reacts to a mouse, stops while disabled or loading, and is off under reduced motion, where it is a plain Button.",
      "whenToUse": [
        "The single primary call to action in a hero or pricing section",
        "A playful secondary action on a marketing page"
      ],
      "whenNotToUse": [
        "Forms, toolbars and dense UI (use button)",
        "Several buttons side by side; one magnetic button per view keeps it meaningful",
        "Rendering a link (not supported; use an <a> with buttonVariants instead)"
      ],
      "composesWith": [
        "button",
        "border-beam"
      ],
      "a11y": [
        {
          "keys": "Enter / Space",
          "action": "Activates the button"
        },
        {
          "keys": "Tab",
          "action": "Moves focus to the button"
        }
      ],
      "customization": [
        "strength: fraction of the pointer offset the button follows (default 0.2)",
        "radius: px outside the button where the pull starts, fading to zero at the edge (default 72)",
        "All Button props: variant, size, shape, loading; wrapperClassName styles the layout wrapper"
      ]
    },
    "examples": [
      "magnetic-button-demo"
    ],
    "url": "https://ui.ballmac.com/components/magnetic-button"
  },
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}