{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "magnet",
  "title": "Magnet",
  "description": "A wrapper that pulls any child toward the pointer from past its own bounds: the whole element, inner layers, a row by distance, or a soft ring that trails.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/magnet.tsx",
      "content": "import type * as React from \"react\"\nimport { useCallback, useContext, useEffect, useMemo, useRef } from \"react\"\nimport { motion, useMotionValue, useReducedMotion, useTransform } from \"motion/react\"\nimport { MagnetBodyContext, MagnetGroupContext, useDocumentPointer, useMagnetForce, type PointerListener } from \"@/registry/miseui/ui/magnet-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype MagnetVariant = \"pull\" | \"layers\" | \"field\" | \"halo\"\ntype MagnetEffect = \"sticky\"\n\nconst DEFAULT_REACH = 96\nconst DEFAULT_STRENGTH: Record<MagnetVariant, number> = { pull: 0.35, layers: 0.12, field: 0.25, halo: 0.2 }\n/** A field sits in a row with gaps of about 8 px, and an item that travels further lands on its neighbour. */\nconst DEFAULT_MAX_OFFSET: Record<MagnetVariant, number> = { pull: 16, layers: 6, field: 4, halo: 10 }\n\ntype MagnetProps = Omit<React.ComponentProps<\"div\">, \"ref\"> & {\n  /** `pull` the whole element drifts, `layers` inner MagnetLayer parts move further than the surface, `field` reacts inside a MagnetGroup, `halo` the element stays and a soft ring trails. */\n  variant?: MagnetVariant\n  /** `sticky` latches once close and lets go only past a wider radius. */\n  effect?: MagnetEffect\n  /** How far past its own bounds it reacts, in px. Ignored inside a MagnetGroup, which sets it for the row. */\n  reach?: number\n  /** The share of the distance to the pointer it moves. */\n  strength?: number\n  /** The furthest it travels, in px. */\n  maxOffset?: number\n  /** For the `halo` ring, to match the corners of what it surrounds. */\n  haloClassName?: string\n}\n\n/** Blurred once and moved by transform. The blur itself never animates, so moving it costs only a compositor layer. */\nconst HALO_CLASS =\n  \"pointer-events-none absolute -inset-2 -z-10 rounded-2xl border-[5px] border-mise blur-[4px] [@media(prefers-reduced-transparency:reduce)]:hidden\"\n\nfunction Magnet({ variant: variantProp, effect, reach: reachProp, strength, maxOffset, haloClassName, className, children, ...props }: MagnetProps) {\n  const group = useContext(MagnetGroupContext)\n  const reduceMotion = useReducedMotion() ?? false\n  const variant = variantProp ?? (group ? \"field\" : \"pull\")\n  const reach = group?.reach ?? reachProp ?? DEFAULT_REACH\n  const rootRef = useRef<HTMLDivElement>(null)\n  const force = useMagnetForce(rootRef, {\n    reach,\n    strength: strength ?? DEFAULT_STRENGTH[variant],\n    maxOffset: maxOffset ?? DEFAULT_MAX_OFFSET[variant],\n    sticky: effect === \"sticky\",\n  })\n  const { move, end, x, y, near } = force\n  const listener = useMemo<PointerListener>(() => ({ move, end }), [move, end])\n  // Inside a group the row owns one listener; a lone magnet brings its own.\n  useDocumentPointer(rootRef, listener, !group && !reduceMotion)\n  useEffect(() => (group && !reduceMotion ? group.add(listener) : undefined), [group, listener, reduceMotion])\n  const bodyValue = useMemo(() => ({ x, y }), [x, y])\n  const scale = useTransform(near, [0, 1], [1, 1.06])\n  const haloOpacity = useTransform(near, [0, 1], [0, 0.55])\n  const style = variant === \"halo\" ? undefined : variant === \"field\" ? { x, y, scale } : { x, y }\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"magnet\"\n      data-variant={variant}\n      data-effect={effect}\n      data-latched={force.latched ? \"true\" : undefined}\n      className={cn(\"relative isolate inline-block max-w-full align-middle\", className)}\n      {...props}\n    >\n      {variant === \"halo\" ? <motion.span aria-hidden=\"true\" data-slot=\"magnet-halo\" className={cn(HALO_CLASS, haloClassName)} style={{ x, y, opacity: haloOpacity }} /> : null}\n      <motion.div data-slot=\"magnet-body\" style={style}>\n        <MagnetBodyContext.Provider value={bodyValue}>{children}</MagnetBodyContext.Provider>\n      </motion.div>\n    </div>\n  )\n}\n\ntype MagnetLayerProps = Omit<React.ComponentProps<\"div\">, \"ref\" | \"style\"> & {\n  /** How much further than the surface this part moves. 1 doubles the surface's travel. */\n  depth?: number\n  style?: React.CSSProperties\n}\n\nfunction MagnetLayer({ depth = 1, className, style, ...props }: MagnetLayerProps) {\n  const body = useContext(MagnetBodyContext)\n  const still = useMotionValue(0)\n  const x = useTransform(body?.x ?? still, (value) => value * depth)\n  const y = useTransform(body?.y ?? still, (value) => value * depth)\n  return (\n    <motion.div\n      data-slot=\"magnet-layer\"\n      className={className}\n      style={{ ...style, x, y }}\n      {...(props as React.ComponentProps<typeof motion.div>)}\n    />\n  )\n}\n\ntype MagnetGroupProps = React.ComponentProps<\"div\"> & {\n  /** How far each item reacts, in px. */\n  reach?: number\n}\n\n/** One listener for the whole row, so twenty items cost one event handler. */\nfunction MagnetGroup({ reach = 120, className, children, ...props }: MagnetGroupProps) {\n  const rootRef = useRef<HTMLDivElement>(null)\n  const reduceMotion = useReducedMotion() ?? false\n  const listeners = useRef<readonly PointerListener[]>([])\n  const add = useCallback((listener: PointerListener) => {\n    listeners.current = [...listeners.current, listener]\n    return () => {\n      listeners.current = listeners.current.filter((entry) => entry !== listener)\n    }\n  }, [])\n  const shared = useMemo<PointerListener>(\n    () => ({\n      move: (point) => listeners.current.forEach((listener) => listener.move(point)),\n      end: () => listeners.current.forEach((listener) => listener.end()),\n    }),\n    [],\n  )\n  useDocumentPointer(rootRef, shared, !reduceMotion)\n  const value = useMemo(() => ({ reach, add }), [reach, add])\n  return (\n    <MagnetGroupContext.Provider value={value}>\n      <div ref={rootRef} data-slot=\"magnet-group\" className={cn(\"flex min-w-0 flex-wrap items-center gap-2\", className)} {...props}>\n        {children}\n      </div>\n    </MagnetGroupContext.Provider>\n  )\n}\n\nexport { Magnet, MagnetLayer, MagnetGroup, type MagnetProps, type MagnetLayerProps, type MagnetGroupProps, type MagnetVariant, type MagnetEffect }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/magnet-math.ts",
      "content": "export interface Point {\n  x: number\n  y: number\n}\n\nexport interface Box {\n  left: number\n  top: number\n  right: number\n  bottom: number\n}\n\n/** How far a point is from a box. Zero anywhere inside it. */\nexport function distanceToBox(point: Point, box: Box) {\n  const dx = Math.max(box.left - point.x, 0, point.x - box.right)\n  const dy = Math.max(box.top - point.y, 0, point.y - box.bottom)\n  return Math.hypot(dx, dy)\n}\n\n/** Full strength on and inside the element, easing to nothing at `reach` with no visible start or stop. */\nexport function magnetFalloff(distance: number, reach: number) {\n  if (reach <= 0) return distance <= 0 ? 1 : 0\n  const progress = Math.min(1, Math.max(0, distance / reach))\n  const inverse = 1 - progress\n  return inverse * inverse * (3 - 2 * inverse)\n}\n\n/**\n * How far the element moves toward the pointer. Capped, because a wide element would otherwise travel by half\n * its own width when the pointer reaches its far edge.\n */\nexport function magnetPull(point: Point, box: Box, strength: number, falloff: number, maxOffset: number): Point {\n  const x = (point.x - (box.left + box.right) / 2) * strength * falloff\n  const y = (point.y - (box.top + box.bottom) / 2) * strength * falloff\n  const length = Math.hypot(x, y)\n  if (length <= maxOffset) return { x, y }\n  return { x: (x / length) * maxOffset, y: (y / length) * maxOffset }\n}\n\n/**\n * Two radii give the latch a dead band: a pointer resting on one circle would otherwise flip it every frame.\n * Distance comes from the still wrapper, so the moving element cannot change its own answer.\n */\nexport function nextLatched(latched: boolean, distance: number, latchRadius: number, releaseRadius: number) {\n  return latched ? distance <= releaseRadius : distance <= latchRadius\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/magnet-state.ts",
      "content": "import type * as React from \"react\"\nimport { createContext, useCallback, useEffect, useRef, useState } from \"react\"\nimport { useMotionValue, useSpring, type MotionValue } from \"motion/react\"\nimport { distanceToBox, magnetFalloff, magnetPull, nextLatched, type Point } from \"@/registry/miseui/ui/magnet-math\"\nimport { isFinePointer } from \"@/registry/miseui/ui/pointer-fine\"\n\n/** Critical damping with the 0.35 s response of SPRING_DEFAULT. useSpring takes physics, not a duration. */\nconst FOLLOW_SPRING = { stiffness: 322, damping: 36, mass: 1 }\nconst LATCHED_STRENGTH = 0.6\n\nexport interface PointerListener {\n  move: (point: Point) => void\n  end: () => void\n}\n\nexport interface MagnetGroupValue {\n  reach: number\n  add: (listener: PointerListener) => () => void\n}\n\nexport const MagnetGroupContext = createContext<MagnetGroupValue | null>(null)\nexport const MagnetBodyContext = createContext<{ x: MotionValue<number>; y: MotionValue<number> } | null>(null)\n\n/**\n * The reach goes past the element's bounds, so the listener is on the document and passive: it can never take a click\n * or a scroll from a neighbour. It exists only while the element is on screen and the input has a fine pointer.\n */\nexport function useDocumentPointer(rootRef: React.RefObject<HTMLElement | null>, listener: PointerListener, enabled: boolean) {\n  const { move, end } = listener\n  useEffect(() => {\n    const root = rootRef.current\n    if (!enabled || !root) return\n    const state = { onScreen: typeof IntersectionObserver === \"undefined\", attached: false }\n    const onMove = (event: PointerEvent) => {\n      if (event.pointerType !== \"touch\") move({ x: event.clientX, y: event.clientY })\n    }\n    const sync = () => {\n      const wanted = state.onScreen && isFinePointer()\n      if (wanted === state.attached) return\n      state.attached = wanted\n      if (wanted) document.addEventListener(\"pointermove\", onMove, { passive: true })\n      else {\n        document.removeEventListener(\"pointermove\", onMove)\n        end()\n      }\n    }\n    const view = typeof IntersectionObserver === \"undefined\" ? null : new IntersectionObserver(([entry]) => {\n      state.onScreen = entry?.isIntersecting ?? true\n      sync()\n    })\n    view?.observe(root)\n    const media = window.matchMedia(\"(hover: hover) and (pointer: fine)\")\n    media.addEventListener?.(\"change\", sync)\n    document.documentElement.addEventListener(\"pointerleave\", end)\n    sync()\n    return () => {\n      view?.disconnect()\n      media.removeEventListener?.(\"change\", sync)\n      document.documentElement.removeEventListener(\"pointerleave\", end)\n      document.removeEventListener(\"pointermove\", onMove)\n    }\n  }, [rootRef, move, end, enabled])\n}\n\nexport interface MagnetOptions {\n  reach: number\n  strength: number\n  maxOffset: number\n  sticky: boolean\n}\n\n/** The values live in motion values, so a pointer stream re-renders nothing except when the latch flips. */\nexport function useMagnetForce(rootRef: React.RefObject<HTMLElement | null>, { reach, strength, maxOffset, sticky }: MagnetOptions) {\n  const targetX = useMotionValue(0)\n  const targetY = useMotionValue(0)\n  const targetNear = useMotionValue(0)\n  const x = useSpring(targetX, FOLLOW_SPRING)\n  const y = useSpring(targetY, FOLLOW_SPRING)\n  const near = useSpring(targetNear, FOLLOW_SPRING)\n  const latchedRef = useRef(false)\n  const [latched, setLatched] = useState(false)\n\n  const setLatch = useCallback((next: boolean) => {\n    if (next === latchedRef.current) return\n    latchedRef.current = next\n    setLatched(next)\n  }, [])\n\n  const end = useCallback(() => {\n    setLatch(false)\n    targetX.set(0)\n    targetY.set(0)\n    targetNear.set(0)\n  }, [setLatch, targetNear, targetX, targetY])\n\n  const move = useCallback(\n    (point: Point) => {\n      const root = rootRef.current\n      if (!root) return\n      const box = root.getBoundingClientRect()\n      const distance = distanceToBox(point, box)\n      const hold = sticky && nextLatched(latchedRef.current, distance, reach * 0.15, reach * 0.6)\n      if (sticky) setLatch(hold)\n      const falloff = hold ? 1 : magnetFalloff(distance, reach)\n      // Far away and already at rest, which is nearly every event on a long page.\n      if (falloff === 0 && targetX.get() === 0 && targetY.get() === 0 && targetNear.get() === 0) return\n      const pull = magnetPull(point, box, hold ? LATCHED_STRENGTH : strength, falloff, hold ? maxOffset * 2 : maxOffset)\n      targetX.set(pull.x)\n      targetY.set(pull.y)\n      targetNear.set(falloff)\n    },\n    [maxOffset, reach, rootRef, setLatch, sticky, strength, targetNear, targetX, targetY],\n  )\n\n  return { x, y, near, latched, move, end }\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/pointer-fine.ts",
      "content": "/**\n * Asked when an event arrives, never during render, so a server render hydrates and a laptop with a touch screen\n * answers for the input it is using right now.\n */\nexport function isFinePointer() {\n  return typeof window !== \"undefined\" && window.matchMedia(\"(hover: hover) and (pointer: fine)\").matches\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "interactions"
  ],
  "type": "registry:ui"
}