{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "magnetic-action",
  "type": "registry:ui",
  "title": "Magnetic action",
  "description": "Lets a primary action lean toward a nearby pointer and settle cleanly on release.",
  "dependencies": [
    "gsap"
  ],
  "categories": [
    "actions"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/magnetic-action/",
  "meta": {
    "engines": [
      "gsap"
    ],
    "runtimeCost": "light",
    "signature": "GSAP pointer tracking with a clean return",
    "sceneFamily": "product-mono",
    "motionRole": "snap",
    "primaryState": "Primary action responding to a nearby pointer",
    "assetProvenance": "self-contained"
  },
  "files": [
    {
      "path": "src/registry/components/magnetic-action.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/magnetic-action.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, type ButtonHTMLAttributes, type PointerEvent } from \"react\";\nimport { gsap } from \"gsap\";\n\nexport type MagneticActionProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"onBlur\" | \"onPointerLeave\" | \"onPointerMove\"> & {\n  strength?: number;\n  labelShift?: number;\n  glow?: string;\n};\n\nfunction useReducedMotionPreference() {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const update = () => setReduced(query.matches);\n    update();\n    query.addEventListener(\"change\", update);\n    return () => query.removeEventListener(\"change\", update);\n  }, []);\n  return reduced;\n}\n\nexport function MagneticAction({\n  children,\n  className = \"\",\n  strength = 0.22,\n  labelShift = 0.42,\n  glow = \"rgba(255,255,255,.46)\",\n  disabled,\n  style,\n  ...props\n}: MagneticActionProps) {\n  const root = useRef<HTMLButtonElement>(null);\n  const label = useRef<HTMLSpanElement>(null);\n  const reduced = useReducedMotionPreference();\n  const movers = useRef<{ x: (value: number) => void; y: (value: number) => void; lx: (value: number) => void; ly: (value: number) => void } | null>(null);\n\n  useEffect(() => {\n    const button = root.current;\n    const text = label.current;\n    if (!button || !text) return;\n    if (reduced || disabled) {\n      movers.current = null;\n      gsap.killTweensOf([button, text]);\n      gsap.set([button, text], { clearProps: \"transform\" });\n      return;\n    }\n    movers.current = {\n      x: gsap.quickTo(button, \"x\", { duration: 0.42, ease: \"power3.out\" }),\n      y: gsap.quickTo(button, \"y\", { duration: 0.42, ease: \"power3.out\" }),\n      lx: gsap.quickTo(text, \"x\", { duration: 0.5, ease: \"power3.out\" }),\n      ly: gsap.quickTo(text, \"y\", { duration: 0.5, ease: \"power3.out\" }),\n    };\n    return () => {\n      movers.current = null;\n      gsap.killTweensOf([button, text]);\n      gsap.set([button, text], { clearProps: \"transform\" });\n    };\n  }, [disabled, reduced]);\n\n  const move = (event: PointerEvent<HTMLButtonElement>) => {\n    if (reduced || disabled || event.pointerType === \"touch\") return;\n    const bounds = event.currentTarget.getBoundingClientRect();\n    const dx = event.clientX - (bounds.left + bounds.width / 2);\n    const dy = event.clientY - (bounds.top + bounds.height / 2);\n    movers.current?.x(dx * strength);\n    movers.current?.y(dy * strength);\n    movers.current?.lx(dx * strength * labelShift);\n    movers.current?.ly(dy * strength * labelShift);\n    event.currentTarget.style.setProperty(\"--magnetic-x\", `${((event.clientX - bounds.left) / bounds.width) * 100}%`);\n    event.currentTarget.style.setProperty(\"--magnetic-y\", `${((event.clientY - bounds.top) / bounds.height) * 100}%`);\n  };\n\n  const reset = () => {\n    movers.current?.x(0);\n    movers.current?.y(0);\n    movers.current?.lx(0);\n    movers.current?.ly(0);\n    root.current?.style.setProperty(\"--magnetic-x\", \"50%\");\n    root.current?.style.setProperty(\"--magnetic-y\", \"50%\");\n  };\n\n  return (\n    <button\n      ref={root}\n      type=\"button\"\n      disabled={disabled}\n      onPointerMove={move}\n      onPointerLeave={reset}\n      onBlur={reset}\n      className={`group relative inline-flex min-h-11 select-none items-center justify-center overflow-hidden rounded-full bg-neutral-900 px-5 text-[13px] font-medium text-white shadow-[0_1px_2px_rgba(0,0,0,.06)] outline-none transition-[box-shadow,background-color] duration-150 focus-visible:shadow-[0_0_0_3px_rgba(69,104,255,.28),0_14px_34px_-18px_rgba(28,25,23,.8)] disabled:pointer-events-none disabled:opacity-45 dark:bg-neutral-100 dark:text-neutral-950 ${className}`}\n      style={{\n        ...style,\n        backgroundImage: `radial-gradient(circle at var(--magnetic-x,50%) var(--magnetic-y,50%), ${glow}, transparent 34%)`,\n        touchAction: \"manipulation\",\n      }}\n      {...props}\n    >\n      <span ref={label} className=\"relative z-10 inline-flex items-center gap-2 will-change-transform\">\n        {children}\n      </span>\n      <span aria-hidden className=\"pointer-events-none absolute inset-[1px] rounded-full border border-white/15\" />\n    </button>\n  );\n}\n"
    }
  ]
}
