{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sticky",
  "type": "registry:ui",
  "title": "StickyButton",
  "description": "A magnetic button whose body and label are pulled toward the cursor from a wide field, with parallax depth and tilt.",
  "files": [
    {
      "path": "components/evil-buttons/sticky.tsx",
      "type": "registry:ui",
      "target": "components/evil-buttons/sticky.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  motion,\n  useMotionTemplate,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface StickyButtonProps\n  extends Omit<\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\"\n  > {\n  /** Distance in px beyond the button's edge where the pull starts. */\n  radius?: number;\n  /** How far the body follows the cursor, as a fraction of the cursor offset. */\n  strength?: number;\n  /** How far the label drifts inside the body, from 0 (fixed) to 1 (edge of the padding). */\n  parallax?: number;\n  /** Maximum tilt in degrees while the body is pulled. */\n  tilt?: number;\n}\n\n// Largest label drift at `parallax = 1`, kept inside the button's padding.\nconst MAX_LABEL_SHIFT = { x: 12, y: 5 };\n\nconst BODY_SPRING = { stiffness: 170, damping: 16, mass: 0.6 };\n// The label lags a little behind the body so the two layers visibly separate.\nconst LABEL_SPRING = { stiffness: 120, damping: 14, mass: 0.7 };\n\n/** 1 inside the button, easing smoothly to 0 at the edge of the field. */\nfunction falloff(t: number) {\n  if (t <= 0) return 1;\n  if (t >= 1) return 0;\n  return 1 - t * t * (3 - 2 * t);\n}\n\nconst StickyButton = React.forwardRef<HTMLButtonElement, StickyButtonProps>(\n  (\n    {\n      children,\n      className,\n      radius = 120,\n      strength = 0.45,\n      parallax = 0.35,\n      tilt = 8,\n      disabled,\n      type = \"button\",\n      ...props\n    },\n    ref,\n  ) => {\n    const wrapperRef = React.useRef<HTMLSpanElement>(null);\n    const reduceMotion = useReducedMotion();\n\n    const pullX = useMotionValue(0);\n    const pullY = useMotionValue(0);\n    const labelPullX = useMotionValue(0);\n    const labelPullY = useMotionValue(0);\n    const glowX = useMotionValue(0);\n    const glowY = useMotionValue(0);\n    const engaged = useMotionValue(0);\n\n    const x = useSpring(pullX, BODY_SPRING);\n    const y = useSpring(pullY, BODY_SPRING);\n    const labelX = useSpring(labelPullX, LABEL_SPRING);\n    const labelY = useSpring(labelPullY, LABEL_SPRING);\n    const glowOpacity = useSpring(engaged, { stiffness: 200, damping: 30 });\n\n    const reach = Math.max(radius, 1) * strength;\n    const rotateY = useTransform(x, [-reach, reach], [-tilt, tilt]);\n    const rotateX = useTransform(y, [-reach, reach], [tilt, -tilt]);\n    const glow = useMotionTemplate`radial-gradient(120px circle at ${glowX}px ${glowY}px, color-mix(in oklab, var(--foreground) 12%, transparent), transparent 70%)`;\n\n    React.useEffect(() => {\n      if (reduceMotion || disabled) return;\n\n      const release = () => {\n        pullX.set(0);\n        pullY.set(0);\n        labelPullX.set(0);\n        labelPullY.set(0);\n        engaged.set(0);\n      };\n\n      const onPointerMove = (event: PointerEvent) => {\n        if (event.pointerType === \"touch\") return;\n        const wrapper = wrapperRef.current;\n        if (!wrapper) return;\n\n        // The wrapper never moves, so it is a stable resting frame to measure from.\n        const rect = wrapper.getBoundingClientRect();\n        const dx = event.clientX - (rect.left + rect.width / 2);\n        const dy = event.clientY - (rect.top + rect.height / 2);\n        const outsideX = Math.max(Math.abs(dx) - rect.width / 2, 0);\n        const outsideY = Math.max(Math.abs(dy) - rect.height / 2, 0);\n        const pull = falloff(Math.hypot(outsideX, outsideY) / radius);\n\n        if (pull === 0) {\n          release();\n          return;\n        }\n\n        pullX.set(dx * strength * pull);\n        pullY.set(dy * strength * pull);\n        const nx = Math.max(-1, Math.min(1, dx / (rect.width / 2)));\n        const ny = Math.max(-1, Math.min(1, dy / (rect.height / 2)));\n        labelPullX.set(nx * MAX_LABEL_SHIFT.x * parallax * pull);\n        labelPullY.set(ny * MAX_LABEL_SHIFT.y * parallax * pull);\n        glowX.set(event.clientX - rect.left);\n        glowY.set(event.clientY - rect.top);\n        engaged.set(pull);\n      };\n\n      window.addEventListener(\"pointermove\", onPointerMove, { passive: true });\n      document.documentElement.addEventListener(\"pointerleave\", release);\n      window.addEventListener(\"blur\", release);\n      return () => {\n        window.removeEventListener(\"pointermove\", onPointerMove);\n        document.documentElement.removeEventListener(\"pointerleave\", release);\n        window.removeEventListener(\"blur\", release);\n        release();\n      };\n    }, [\n      radius,\n      strength,\n      parallax,\n      reduceMotion,\n      disabled,\n      pullX,\n      pullY,\n      labelPullX,\n      labelPullY,\n      glowX,\n      glowY,\n      engaged,\n    ]);\n\n    return (\n      <span\n        ref={wrapperRef}\n        className=\"relative inline-flex [perspective:600px]\"\n      >\n        <motion.button\n          ref={ref}\n          type={type}\n          disabled={disabled}\n          style={{ x, y, rotateX, rotateY }}\n          whileTap={disabled ? undefined : { scale: 0.95 }}\n          transition={{ type: \"spring\", stiffness: 400, damping: 22 }}\n          className={cn(\n            \"relative inline-flex h-9 cursor-pointer items-center justify-center rounded-md border border-dashed border-border bg-background px-4 text-sm font-medium text-foreground shadow-[0_1px_2px_rgb(0_0_0/0.05),0_2px_6px_-3px_rgb(0_0_0/0.07),inset_0_-1px_0_rgb(0_0_0/0.03)] dark:shadow-[0_1px_2px_rgb(0_0_0/0.4),inset_0_1px_0_rgb(255_255_255/0.05)] outline-none select-none transform-3d\",\n            \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50\",\n            className,\n          )}\n          {...props}\n        >\n          {/* Soft spotlight that tracks the cursor across the face. */}\n          <motion.span\n            aria-hidden\n            className=\"pointer-events-none absolute inset-0 rounded-[inherit]\"\n            style={{ backgroundImage: glow, opacity: glowOpacity }}\n          />\n          <motion.span\n            className=\"relative inline-flex items-center gap-1.5 whitespace-nowrap\"\n            style={{ x: labelX, y: labelY }}\n          >\n            {children}\n          </motion.span>\n        </motion.button>\n      </span>\n    );\n  },\n);\n\nStickyButton.displayName = \"StickyButton\";\n\nexport { StickyButton };\nexport default StickyButton;\n"
    }
  ],
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ]
}
