{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "DodgeField-JS-CSS",
	"title": "DodgeField",
	"description": "Wrapper that makes any child flee the pointer inside a bounded field, dodges once per approach, then relents after a few tries and glides home.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "DodgeField.css",
			"target": "@components/DodgeField.css",
			"content": ".dodge-field {\n  --df-ink: #f5f5f5;\n  --df-contrast: #18181b;\n  --df-height: 240px;\n  --df-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n\n  position: relative;\n  display: grid;\n  place-items: center;\n  width: 100%;\n  height: var(--df-height);\n}\n\n.dodge-field__mover {\n  display: inline-grid;\n  will-change: transform;\n}\n\n.dodge-field__pill {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  height: 52px;\n  margin: 0;\n  padding: 0 28px;\n  border: 0;\n  border-radius: 999px;\n  background: color-mix(in srgb, var(--df-ink) 8%, transparent);\n  color: color-mix(in srgb, var(--df-ink) 72%, transparent);\n  font-family: inherit;\n  font-size: 18px;\n  font-weight: 500;\n  line-height: 1;\n  letter-spacing: -0.012em;\n  cursor: pointer;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-tap-highlight-color: transparent;\n  touch-action: manipulation;\n  transition:\n    transform 160ms var(--df-ease-out),\n    background-color 220ms ease,\n    color 220ms ease;\n}\n\n.dodge-field__pill:active {\n  transform: scale(0.97);\n}\n\n.dodge-field__pill:focus-visible {\n  outline: 2px solid color-mix(in srgb, var(--df-ink) 60%, transparent);\n  outline-offset: 3px;\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .dodge-field__pill:hover {\n    color: color-mix(in srgb, var(--df-ink) 90%, transparent);\n  }\n}\n\n.dodge-field__mover[data-relented='true'] .dodge-field__pill {\n  color: var(--df-ink);\n}\n\n.dodge-field__mover[data-caught='true'] .dodge-field__pill {\n  background: var(--df-ink);\n  color: var(--df-contrast);\n}\n\n.dodge-field__labels {\n  display: grid;\n}\n\n.dodge-field__label {\n  grid-area: 1 / 1;\n  white-space: nowrap;\n  transition:\n    opacity 200ms ease,\n    filter 200ms ease;\n}\n\n.dodge-field__label[data-active='false'] {\n  opacity: 0;\n  filter: blur(2px);\n}\n\n.dodge-field__notice {\n  position: absolute;\n  inset: auto 0 12px;\n  margin: 0;\n  color: color-mix(in srgb, var(--df-ink) 55%, transparent);\n  font-size: 12px;\n  text-align: center;\n  pointer-events: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .dodge-field__pill {\n    transition:\n      background-color 220ms ease,\n      color 220ms ease;\n  }\n\n  .dodge-field__pill:active {\n    transform: none;\n  }\n\n  .dodge-field .dodge-field__label {\n    filter: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "DodgeField.jsx",
			"content": "'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { animate, motion, useMotionTemplate, useMotionValue, useReducedMotion } from 'motion/react';\n\nimport './DodgeField.css';\n\nconst COUNT_LINE = 0.55;\nconst DEAD_ZONE = 6;\nconst CAUGHT_HOLD_MS = 760;\nconst INSET = 12;\nconst DEFAULT_TAUNTS = ['Catch me', 'Nope', 'Too slow', 'Almost', 'Okay, okay'];\n\nconst wallIt = (t, room, wall) => {\n  if (wall === 'bounce') {\n    if (t > room) return Math.max(-room, 2 * room - t);\n    if (t < -room) return Math.min(room, -2 * room - t);\n    return t;\n  }\n  return Math.min(room, Math.max(-room, t));\n};\nconst bearingOf = (dx, dy, d, axis) => {\n  if (axis === 'x') return { x: Math.sign(dx) || 1, y: 0 };\n  if (axis === 'y') return { x: 0, y: Math.sign(dy) || 1 };\n  return { x: dx / d, y: dy / d };\n};\n\nexport default function DodgeField({\n  children,\n  taunts = DEFAULT_TAUNTS,\n  notice = '',\n  inkColor = '#f5f5f5',\n  contrastColor = '#18181b',\n  fieldHeight = 240,\n  reach = 72,\n  radius = 120,\n  falloff = 2,\n  fleeDuration = 130,\n  returnDuration = 620,\n  returnBounce = 0.1,\n  axis = 'both',\n  wall = 'clamp',\n  patience = 4,\n  disabled = false,\n  onDodge,\n  onRelent,\n  onCatch,\n  className = '',\n  style\n}) {\n  const fieldRef = useRef(null);\n  const moverRef = useRef(null);\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const transform = useMotionTemplate`translate(${x}px, ${y}px)`;\n  const reduce = useReducedMotion();\n  const [fine, setFine] = useState(true);\n  const [inside, setInside] = useState(false);\n  const [dodges, setDodges] = useState(0);\n  const [caught, setCaught] = useState(false);\n  const gave = dodges >= Math.max(1, patience);\n  const still = gave || caught || disabled || !!reduce;\n\n  const pointer = useRef(null);\n  const bearing = useRef({ x: 1, y: 0 });\n  const armed = useRef(true);\n  const room = useRef({ x: 0, y: 0 });\n  const raf = useRef(0);\n  const hold = useRef(undefined);\n  const live = useRef({});\n  live.current = {\n    reach,\n    radius,\n    falloff,\n    fleeDuration,\n    returnDuration,\n    returnBounce,\n    axis,\n    wall,\n    still,\n    inside,\n    reduce\n  };\n\n  useEffect(() => {\n    const field = fieldRef.current;\n    const mover = moverRef.current;\n    if (!field || !mover) return undefined;\n    const measure = () => {\n      room.current = {\n        x: Math.max(0, (field.clientWidth - mover.offsetWidth) / 2 - INSET),\n        y: Math.max(0, (field.clientHeight - mover.offsetHeight) / 2 - INSET)\n      };\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(field);\n    observer.observe(mover);\n    return () => observer.disconnect();\n  }, []);\n\n  const frame = useCallback(() => {\n    raf.current = 0;\n    const field = fieldRef.current;\n    const p = pointer.current;\n    const L = live.current;\n    if (!field) return;\n    const rect = field.getBoundingClientRect();\n    const zoom = rect.width / (field.offsetWidth || rect.width) || 1;\n    const dx = p ? (p.x - (rect.left + rect.width / 2)) / zoom : Infinity;\n    const dy = p ? (p.y - (rect.top + rect.height / 2)) / zoom : Infinity;\n    const d = Math.hypot(dx, dy);\n    const isInside = d <= L.radius;\n    if (!isInside && !L.inside) return;\n    if (!L.still) {\n      if (d < L.radius * COUNT_LINE) {\n        if (armed.current) {\n          armed.current = false;\n          setDodges(n => n + 1);\n        }\n      } else if (d > L.radius) {\n        armed.current = true;\n      }\n    }\n    if (Number.isFinite(d) && d > DEAD_ZONE) bearing.current = bearingOf(dx, dy, d, L.axis);\n    const flee = isInside && !L.still ? (1 - d / L.radius) ** L.falloff : 0;\n    const tx = wallIt(-bearing.current.x * flee * L.reach, room.current.x, L.wall);\n    const ty = wallIt(-bearing.current.y * flee * L.reach, room.current.y, L.wall);\n    if (L.reduce) {\n      x.jump(0);\n      y.jump(0);\n    } else {\n      const cfg =\n        flee > 0\n          ? { type: 'spring', duration: L.fleeDuration / 1000, bounce: 0 }\n          : { type: 'spring', duration: L.returnDuration / 1000, bounce: L.returnBounce };\n      animate(x, tx, cfg);\n      animate(y, ty, cfg);\n    }\n    if (isInside !== L.inside) setInside(isInside);\n  }, [x, y]);\n\n  useEffect(() => {\n    const query = window.matchMedia('(hover: hover) and (pointer: fine)');\n    const sync = () => setFine(query.matches);\n    sync();\n    query.addEventListener('change', sync);\n    if (!query.matches || disabled) return () => query.removeEventListener('change', sync);\n    const tick = () => {\n      if (!raf.current) raf.current = requestAnimationFrame(frame);\n    };\n    const onMove = e => {\n      if (e.pointerType === 'touch') return;\n      pointer.current = { x: e.clientX, y: e.clientY };\n      tick();\n    };\n    const onLeave = () => {\n      pointer.current = null;\n      tick();\n    };\n    window.addEventListener('pointermove', onMove, { passive: true });\n    window.addEventListener('scroll', tick, { passive: true, capture: true });\n    document.addEventListener('pointerleave', onLeave);\n    window.addEventListener('blur', onLeave);\n    return () => {\n      query.removeEventListener('change', sync);\n      window.removeEventListener('pointermove', onMove);\n      window.removeEventListener('scroll', tick, { capture: true });\n      document.removeEventListener('pointerleave', onLeave);\n      window.removeEventListener('blur', onLeave);\n      cancelAnimationFrame(raf.current);\n      raf.current = 0;\n    };\n  }, [disabled, frame]);\n\n  useEffect(() => {\n    if (!raf.current) raf.current = requestAnimationFrame(frame);\n  }, [still, frame]);\n  useEffect(() => {\n    if (dodges) onDodge?.(dodges);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [dodges]);\n  useEffect(() => {\n    if (gave) onRelent?.();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [gave]);\n  useEffect(() => () => clearTimeout(hold.current), []);\n\n  const handleClick = () => {\n    setCaught(true);\n    onCatch?.();\n    clearTimeout(hold.current);\n    hold.current = setTimeout(() => {\n      setCaught(false);\n      setDodges(0);\n      armed.current = false;\n    }, CAUGHT_HOLD_MS);\n  };\n\n  const state = { dodges, gave, caught, fleeing: inside && !still };\n  const index = gave || caught ? taunts.length - 1 : Math.min(dodges, Math.max(0, taunts.length - 2));\n  const content =\n    typeof children === 'function'\n      ? children(state)\n      : (children ?? (\n          <button type=\"button\" className=\"dodge-field__pill\" aria-label={taunts[0]}>\n            <span className=\"dodge-field__labels\">\n              {taunts.map((taunt, i) => (\n                <span\n                  key={`${taunt}-${i}`}\n                  className=\"dodge-field__label\"\n                  data-active={i === index ? 'true' : 'false'}\n                  aria-hidden=\"true\"\n                >\n                  {taunt}\n                </span>\n              ))}\n            </span>\n          </button>\n        ));\n\n  return (\n    <div\n      ref={fieldRef}\n      className={`dodge-field${className ? ` ${className}` : ''}`}\n      data-coarse={fine ? undefined : ''}\n      data-flat={reduce ? '' : undefined}\n      style={{ '--df-ink': inkColor, '--df-contrast': contrastColor, '--df-height': `${fieldHeight}px`, ...style }}\n    >\n      <motion.div\n        ref={moverRef}\n        className=\"dodge-field__mover\"\n        style={{ transform }}\n        data-fled={inside && !still ? 'true' : 'false'}\n        data-relented={gave ? 'true' : 'false'}\n        data-caught={caught ? 'true' : 'false'}\n        onClick={handleClick}\n      >\n        {content}\n      </motion.div>\n      {!fine && notice ? <p className=\"dodge-field__notice\">{notice}</p> : null}\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12"
	]
}