{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "magnetic-button",
  "type": "registry:ui",
  "title": "Magnetic Button",
  "description": "A pill button pulled toward the cursor once it comes near, its label drifting a little further for parallax depth, then easing back to rest when the cursor leaves the field.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/magnetic-button.tsx",
      "type": "registry:component",
      "target": "components/tweenui/magnetic-button.tsx",
      "content": "'use client';\n\nimport { useRef, type ComponentPropsWithRef } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\n// Touch has no hover to pull toward, and reduced motion asks for stillness.\nconst canMagnetize = () =>\n  typeof window !== 'undefined' &&\n  !window.matchMedia('(prefers-reduced-motion: reduce)').matches &&\n  window.matchMedia('(hover: hover) and (pointer: fine)').matches;\n\nexport interface MagneticButtonProps extends ComponentPropsWithRef<'button'> {\n  /** How strongly the button follows the cursor, as a share of its offset from center. */\n  strength?: number;\n  /** Distance around the button, in pixels, where the pull begins. */\n  field?: number;\n  /** Extra drift of the label on top of the button's own, for depth. */\n  parallax?: number;\n  /** Scale while the cursor is inside the field. */\n  hoverScale?: number;\n  /** How long the button takes to catch up with the cursor, in seconds. */\n  duration?: number;\n}\n\nconst MagneticButton = ({\n  children,\n  className,\n  type = 'button',\n  strength = 0.35,\n  field = 60,\n  parallax = 0.35,\n  hoverScale = 1.05,\n  duration = 0.6,\n  disabled,\n  ref,\n  ...props\n}: MagneticButtonProps) => {\n  const btnRef = useRef<HTMLButtonElement>(null);\n  const labelRef = useRef<HTMLSpanElement>(null);\n\n  // Merge the internal ref (needed for GSAP) with any forwarded ref.\n  const setRef = (node: HTMLButtonElement | null) => {\n    btnRef.current = node;\n    if (typeof ref === 'function') ref(node);\n    else if (ref) ref.current = node;\n  };\n\n  useGSAP(\n    () => {\n      const btn = btnRef.current;\n      const label = labelRef.current;\n      if (!btn || !label || disabled || !canMagnetize()) return;\n\n      const tween = { duration: Math.max(0.05, duration), ease: 'power3.out' };\n      const btnX = gsap.quickTo(btn, 'x', tween);\n      const btnY = gsap.quickTo(btn, 'y', tween);\n      const btnScale = gsap.quickTo(btn, 'scale', tween);\n      const labelX = gsap.quickTo(label, 'x', tween);\n      const labelY = gsap.quickTo(label, 'y', tween);\n\n      const pull = (x: number, y: number, scale: number) => {\n        btnX(x);\n        btnY(y);\n        btnScale(scale);\n        labelX(x * parallax);\n        labelY(y * parallax);\n      };\n\n      const setActive = (active: boolean) => {\n        if (active) btn.setAttribute('data-active', '');\n        else btn.removeAttribute('data-active');\n      };\n\n      const release = () => {\n        setActive(false);\n        pull(0, 0, 1);\n      };\n\n      const onMove = (e: PointerEvent) => {\n        // The button is the thing moving, so undo its current transform to\n        // find where it rests — measuring the moved box would chase itself.\n        const r = btn.getBoundingClientRect();\n        const scale = Number(gsap.getProperty(btn, 'scale')) || 1;\n        const cx = r.left + r.width / 2 - Number(gsap.getProperty(btn, 'x'));\n        const cy = r.top + r.height / 2 - Number(gsap.getProperty(btn, 'y'));\n        const halfW = r.width / scale / 2;\n        const halfH = r.height / scale / 2;\n\n        // Distance from the pointer to the resting edge — 0 when inside.\n        const dx = Math.max(Math.abs(e.clientX - cx) - halfW, 0);\n        const dy = Math.max(Math.abs(e.clientY - cy) - halfH, 0);\n        if (Math.hypot(dx, dy) > Math.max(0, field)) return release();\n\n        setActive(true);\n        pull((e.clientX - cx) * strength, (e.clientY - cy) * strength, hoverScale);\n      };\n\n      window.addEventListener('pointermove', onMove, { passive: true });\n      document.documentElement.addEventListener('pointerleave', release);\n      window.addEventListener('blur', release);\n\n      return () => {\n        window.removeEventListener('pointermove', onMove);\n        document.documentElement.removeEventListener('pointerleave', release);\n        window.removeEventListener('blur', release);\n        setActive(false);\n      };\n    },\n    {\n      scope: btnRef,\n      dependencies: [strength, field, parallax, hoverScale, duration, disabled],\n      revertOnUpdate: true,\n    }\n  );\n\n  return (\n    <button\n      ref={setRef}\n      type={type}\n      disabled={disabled}\n      className={cn(\n        'inline-flex h-12 cursor-pointer items-center justify-center rounded-full border border-transparent bg-[#045f64] px-6 text-sm font-medium whitespace-nowrap text-[#c6f56f] shadow-[0_1px_1px_rgba(16,24,40,0.16)] transition-colors duration-300 will-change-transform select-none focus-visible:bg-[#c6f56f] focus-visible:text-[#045f64] focus-visible:ring-2 focus-visible:ring-[#045f64] focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60 data-[active]:bg-[#c6f56f] data-[active]:text-[#045f64] motion-reduce:transition-none dark:ring-offset-[#12161F]',\n        className\n      )}\n      {...props}\n    >\n      <span ref={labelRef} className=\"inline-block will-change-transform\">\n        {children}\n      </span>\n    </button>\n  );\n};\n\nexport default MagneticButton;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/magnetic-button"
}