{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "troll-button",
  "type": "registry:ui",
  "title": "TrollButton",
  "description": "A button that dodges the cursor until it runs out of stamina, then gives up and lets you click it.",
  "files": [
    {
      "path": "components/evil-buttons/troll-button.tsx",
      "type": "registry:ui",
      "target": "components/evil-buttons/troll-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n  useVelocity,\n  type AnimationPlaybackControls,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface TrollButtonProps\n  extends Omit<\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\"\n  > {\n  /** Label used when no children are provided. */\n  label?: React.ReactNode;\n  /** Label shown once the button gives up and lets itself be clicked. */\n  surrenderLabel?: React.ReactNode;\n  /** Milliseconds of chasing before the button gets tired and gives up. */\n  giveUpAfter?: number;\n  /** How close in px the cursor can get to the button's edge before it dodges. */\n  fleeRadius?: number;\n  /** Maximum distance in px the button strays from its resting spot. */\n  range?: number;\n  /** Milliseconds after a successful click before the button starts fleeing again. Set to 0 to stay caught. */\n  resetAfter?: number;\n}\n\ntype TrollState = \"fleeing\" | \"tired\";\n\nconst DODGE_SPRING = { stiffness: 520, damping: 30, mass: 0.6 };\n\nconst labelVariants = {\n  enter: { y: 8, opacity: 0 },\n  center: { y: 0, opacity: 1 },\n  exit: { y: -8, opacity: 0 },\n};\n\nconst TrollButton = React.forwardRef<HTMLButtonElement, TrollButtonProps>(\n  (\n    {\n      children,\n      label = \"Click Me\",\n      surrenderLabel = \"Fine, click me\",\n      giveUpAfter = 3500,\n      fleeRadius = 56,\n      range = 140,\n      resetAfter = 1500,\n      onClick,\n      onPointerDown,\n      className,\n      disabled,\n      type = \"button\",\n      ...props\n    },\n    ref,\n  ) => {\n    const wrapperRef = React.useRef<HTMLSpanElement>(null);\n    const offsetRef = React.useRef({ x: 0, y: 0 });\n    const staminaRef = React.useRef<AnimationPlaybackControls | null>(null);\n    const resetTimerRef = React.useRef<number | undefined>(undefined);\n    const [state, setState] = React.useState<TrollState>(\"fleeing\");\n    const reduceMotion = useReducedMotion();\n    const evasive = state === \"fleeing\" && !reduceMotion && !disabled;\n\n    const targetX = useMotionValue(0);\n    const targetY = useMotionValue(0);\n    const x = useSpring(targetX, DODGE_SPRING);\n    const y = useSpring(targetY, DODGE_SPRING);\n    // Lean into each dodge based on how fast the button is moving sideways.\n    const velocityX = useVelocity(x);\n    const rotate = useTransform(velocityX, [-2400, 0, 2400], [-10, 0, 10]);\n    const stamina = useMotionValue(1);\n    const staminaOpacity = useTransform(stamina, (value) => (value < 1 ? 1 : 0));\n\n    const moveTo = React.useCallback(\n      (nextX: number, nextY: number) => {\n        offsetRef.current = { x: nextX, y: nextY };\n        targetX.set(nextX);\n        targetY.set(nextY);\n      },\n      [targetX, targetY],\n    );\n\n    const startTiring = React.useCallback(() => {\n      if (staminaRef.current) return;\n      staminaRef.current = animate(stamina, 0, {\n        duration: giveUpAfter / 1000,\n        ease: \"linear\",\n        onComplete: () => {\n          moveTo(0, 0);\n          setState(\"tired\");\n        },\n      });\n    }, [giveUpAfter, moveTo, stamina]);\n\n    const dodge = React.useCallback(\n      (pointerX: number, pointerY: number, force = false) => {\n        const wrapper = wrapperRef.current;\n        if (!wrapper) return;\n\n        // Measure from the resting slot, then add the current offset to find the button.\n        const rest = wrapper.getBoundingClientRect();\n        const { x: offsetX, y: offsetY } = offsetRef.current;\n        const centerX = rest.left + rest.width / 2 + offsetX;\n        const centerY = rest.top + rest.height / 2 + offsetY;\n        const dx = centerX - pointerX;\n        const dy = centerY - pointerY;\n        const outsideX = Math.max(Math.abs(dx) - rest.width / 2, 0);\n        const outsideY = Math.max(Math.abs(dy) - rest.height / 2, 0);\n        const gap = Math.hypot(outsideX, outsideY);\n\n        if (!force && gap >= fleeRadius) return;\n\n        const length = Math.hypot(dx, dy) || 1;\n        const push = fleeRadius - gap + 24;\n        let nextX = offsetX + (dx / length) * push;\n        let nextY = offsetY + (dy / length) * push;\n        const rangeY = range * 0.6;\n\n        // Cornered: slip past the cursor to the opposite side of the slot.\n        if (Math.abs(nextX) > range || Math.abs(nextY) > rangeY) {\n          nextX = -Math.sign(offsetX || dx || 1) * range * 0.7;\n          nextY = -offsetY * 0.5;\n        }\n\n        moveTo(nextX, nextY);\n        startTiring();\n      },\n      [fleeRadius, moveTo, range, startTiring],\n    );\n\n    React.useEffect(() => {\n      if (!evasive) return;\n\n      const onPointerMove = (event: PointerEvent) => {\n        if (event.pointerType === \"touch\") return;\n        dodge(event.clientX, event.clientY);\n      };\n\n      window.addEventListener(\"pointermove\", onPointerMove, { passive: true });\n      return () => window.removeEventListener(\"pointermove\", onPointerMove);\n    }, [dodge, evasive]);\n\n    React.useEffect(\n      () => () => {\n        staminaRef.current?.stop();\n        window.clearTimeout(resetTimerRef.current);\n      },\n      [],\n    );\n\n    const handlePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {\n      onPointerDown?.(event);\n      // Touch has no hover to flee from, so tapping triggers a dodge instead.\n      if (evasive && event.pointerType === \"touch\") {\n        dodge(event.clientX, event.clientY, true);\n      }\n    };\n\n    const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n      // Keyboard activation (detail 0) always goes through.\n      if (evasive && event.detail !== 0) {\n        event.preventDefault();\n        return;\n      }\n      onClick?.(event);\n\n      if (state === \"tired\" && resetAfter > 0) {\n        window.clearTimeout(resetTimerRef.current);\n        resetTimerRef.current = window.setTimeout(() => {\n          staminaRef.current = null;\n          stamina.set(1);\n          setState(\"fleeing\");\n        }, resetAfter);\n      }\n    };\n\n    const idleLabel = children ?? label;\n    const currentLabel = state === \"tired\" ? surrenderLabel : idleLabel;\n\n    return (\n      <span ref={wrapperRef} className=\"relative inline-flex\">\n        <motion.button\n          ref={ref}\n          type={type}\n          disabled={disabled}\n          style={{ x, y, rotate }}\n          onPointerDown={handlePointerDown}\n          onClick={handleClick}\n          animate={\n            state === \"tired\" && !reduceMotion\n              ? { scaleY: [1, 0.92, 1.03, 1], scaleX: [1, 1.05, 0.98, 1] }\n              : undefined\n          }\n          transition={{ duration: 0.45, ease: \"easeOut\" }}\n          className={cn(\n            \"relative inline-flex h-9 cursor-pointer items-center justify-center overflow-hidden rounded-[10px] px-4 text-sm font-medium text-neutral-50 outline-none select-none\",\n            // Graded dark surface: dark outer hairline, faint inner ring, top highlight, soft drop.\n            \"bg-linear-to-b from-[#353535] to-[#272727] shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.06),inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(0_0_0/0.4)]\",\n            // Motion owns `transform` for the dodge, lean, and squash, so the press animates the separate `scale` property.\n            \"transition-[scale,filter,box-shadow] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)] hover:brightness-110\",\n            // Press eases in fast and settles the shadow; release springs back on the slower base curve.\n            \"active:scale-[0.97] active:brightness-95 active:duration-100 active:ease-out active:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.05),inset_0_1px_0_rgb(255_255_255/0.08),0_0_1px_rgb(0_0_0/0.2),0_1px_3px_-2px_rgb(0_0_0/0.3)] motion-reduce:active:scale-100\",\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          <span className=\"relative inline-grid\">\n            {/* Invisible copies reserve the wider label so the button never resizes. */}\n            <span\n              aria-hidden\n              className=\"invisible col-start-1 row-start-1 whitespace-nowrap\"\n            >\n              {idleLabel}\n            </span>\n            <span\n              aria-hidden\n              className=\"invisible col-start-1 row-start-1 whitespace-nowrap\"\n            >\n              {surrenderLabel}\n            </span>\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.span\n                key={state}\n                variants={labelVariants}\n                initial=\"enter\"\n                animate=\"center\"\n                exit=\"exit\"\n                transition={{ type: \"spring\", stiffness: 420, damping: 30 }}\n                className=\"col-start-1 row-start-1 whitespace-nowrap text-center\"\n              >\n                {currentLabel}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n\n          {/* Stamina drains while the button is being chased. */}\n          <motion.span\n            aria-hidden\n            className=\"pointer-events-none absolute inset-x-0 bottom-0 h-0.5 origin-left bg-neutral-50/35\"\n            style={{\n              scaleX: stamina,\n              opacity: state === \"fleeing\" ? staminaOpacity : 0,\n            }}\n          />\n        </motion.button>\n      </span>\n    );\n  },\n);\n\nTrollButton.displayName = \"TrollButton\";\n\nexport { TrollButton };\nexport default TrollButton;\n"
    }
  ],
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ]
}
