{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A MagneticField component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  type MotionValue,\n  motion,\n  motionValue,\n  useReducedMotion,\n} from \"motion/react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type MagneticFieldMode = \"attract\" | \"repel\" | \"orbit\";\n\nexport type MagneticFieldItem = {\n  element: ReactElement;\n  id: string;\n};\n\nexport type MagneticFieldProps = {\n  children?: ReactNode;\n  className?: string;\n  damping?: number;\n  items?: MagneticFieldItem[];\n  mode?: MagneticFieldMode;\n  paused?: boolean;\n  radius?: number;\n  rotate?: boolean;\n  stiffness?: number;\n  strength?: number;\n};\n\ntype ParticipantState = {\n  centerX: number;\n  centerY: number;\n  rotate: MotionValue<number>;\n  vr: number;\n  vx: number;\n  vy: number;\n  x: MotionValue<number>;\n  y: MotionValue<number>;\n};\n\nconst DEFAULT_RADIUS = 160;\nconst DEFAULT_STRENGTH = 0.35;\nconst DEFAULT_STIFFNESS = 180;\nconst DEFAULT_DAMPING = 18;\nconst MAX_ROTATE_DEG = 18;\nconst FALLOFF_POWER = 2;\nconst MAX_DELTA_SECONDS = 0.05;\nconst ORBIT_RADIAL_FACTOR = 0.2;\nconst ORBIT_TANGENTIAL_FACTOR = 0.4;\nconst MAGNETIC_SELECTOR = \"[data-magnetic]\";\n\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\n\nconst computeFalloff = (distance: number, radius: number) => {\n  if (distance >= radius) {\n    return 0;\n  }\n  return (1 - distance / radius) ** FALLOFF_POWER;\n};\n\nconst computeOffset = (\n  dx: number,\n  dy: number,\n  distance: number,\n  falloff: number,\n  strength: number,\n  mode: MagneticFieldMode\n): { x: number; y: number } => {\n  if (distance === 0) {\n    return { x: 0, y: 0 };\n  }\n  const directional = mode === \"repel\" ? -Math.abs(strength) : strength;\n  if (mode === \"orbit\") {\n    const tangentX = -dy / distance;\n    const tangentY = dx / distance;\n    const pull = directional * falloff * distance * ORBIT_TANGENTIAL_FACTOR;\n    return {\n      x: dx * directional * falloff * ORBIT_RADIAL_FACTOR + tangentX * pull,\n      y: dy * directional * falloff * ORBIT_RADIAL_FACTOR + tangentY * pull,\n    };\n  }\n  return { x: dx * directional * falloff, y: dy * directional * falloff };\n};\n\nconst createParticipant = (): ParticipantState => ({\n  centerX: 0,\n  centerY: 0,\n  rotate: motionValue(0),\n  vr: 0,\n  vx: 0,\n  vy: 0,\n  x: motionValue(0),\n  y: motionValue(0),\n});\n\nconst resetParticipant = (state: ParticipantState) => {\n  state.x.set(0);\n  state.y.set(0);\n  state.rotate.set(0);\n  state.vx = 0;\n  state.vy = 0;\n  state.vr = 0;\n};\n\nconst MagneticField = ({\n  children,\n  className,\n  damping = DEFAULT_DAMPING,\n  items,\n  mode = \"attract\",\n  paused = false,\n  radius = DEFAULT_RADIUS,\n  rotate = false,\n  stiffness = DEFAULT_STIFFNESS,\n  strength = DEFAULT_STRENGTH,\n}: MagneticFieldProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isPointerCapable, setIsPointerCapable] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const pointerRef = useRef<{ x: number; y: number } | null>(null);\n  const participantsRef = useRef<Map<string, ParticipantState>>(new Map());\n  const nodeMapRef = useRef<Map<string, HTMLElement>>(new Map());\n  const elementsRef = useRef<Map<HTMLElement, ParticipantState>>(new Map());\n  const rafRef = useRef<number | null>(null);\n  const lastFrameRef = useRef<number | null>(null);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsPointerCapable(mediaQuery.matches);\n    const handleChange = (event: MediaQueryListEvent) => {\n      setIsPointerCapable(event.matches);\n    };\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  const isEffectDisabled = paused || shouldReduceMotion || !isPointerCapable;\n\n  const getOrCreateParticipant = (id: string): ParticipantState => {\n    let state = participantsRef.current.get(id);\n    if (!state) {\n      state = createParticipant();\n      participantsRef.current.set(id, state);\n    }\n    return state;\n  };\n\n  // Prune stale participants when the `items` list changes shape.\n  useEffect(() => {\n    if (!items) {\n      participantsRef.current.clear();\n      nodeMapRef.current.clear();\n      return;\n    }\n    const ids = new Set(items.map((item) => item.id));\n    for (const key of Array.from(participantsRef.current.keys())) {\n      if (!ids.has(key)) {\n        participantsRef.current.delete(key);\n        nodeMapRef.current.delete(key);\n      }\n    }\n  }, [items]);\n\n  // Measure rest positions for `items` participants once per layout change.\n  useEffect(() => {\n    if (!items) {\n      return;\n    }\n    const container = containerRef.current;\n    if (!container) {\n      return;\n    }\n\n    const measure = () => {\n      const containerRect = container.getBoundingClientRect();\n      for (const [id, node] of nodeMapRef.current) {\n        const state = participantsRef.current.get(id);\n        if (!state) {\n          continue;\n        }\n        const rect = node.getBoundingClientRect();\n        state.centerX = rect.left + rect.width / 2 - containerRect.left;\n        state.centerY = rect.top + rect.height / 2 - containerRect.top;\n      }\n    };\n\n    measure();\n    const resizeObserver = new ResizeObserver(measure);\n    for (const node of nodeMapRef.current.values()) {\n      resizeObserver.observe(node);\n    }\n    window.addEventListener(\"resize\", measure);\n\n    return () => {\n      resizeObserver.disconnect();\n      window.removeEventListener(\"resize\", measure);\n    };\n  }, [items]);\n\n  // Discover and measure `data-magnetic` descendants of `children`.\n  useEffect(() => {\n    if (items) {\n      return;\n    }\n    const container = containerRef.current;\n    if (!container) {\n      return;\n    }\n\n    const syncElements = () => {\n      const found = Array.from(\n        container.querySelectorAll<HTMLElement>(MAGNETIC_SELECTOR)\n      );\n      const map = elementsRef.current;\n      for (const el of Array.from(map.keys())) {\n        if (!found.includes(el)) {\n          el.style.transform = \"\";\n          el.style.willChange = \"\";\n          map.delete(el);\n        }\n      }\n      const containerRect = container.getBoundingClientRect();\n      for (const el of found) {\n        if (!map.has(el)) {\n          el.style.willChange = \"transform\";\n          map.set(el, createParticipant());\n        }\n        const state = map.get(el);\n        if (!state) {\n          continue;\n        }\n        const rect = el.getBoundingClientRect();\n        state.centerX = rect.left + rect.width / 2 - containerRect.left;\n        state.centerY = rect.top + rect.height / 2 - containerRect.top;\n      }\n    };\n\n    syncElements();\n    const mutationObserver = new MutationObserver(syncElements);\n    mutationObserver.observe(container, { childList: true, subtree: true });\n    const resizeObserver = new ResizeObserver(syncElements);\n    resizeObserver.observe(container);\n    window.addEventListener(\"resize\", syncElements);\n\n    return () => {\n      mutationObserver.disconnect();\n      resizeObserver.disconnect();\n      window.removeEventListener(\"resize\", syncElements);\n      for (const el of elementsRef.current.keys()) {\n        el.style.transform = \"\";\n        el.style.willChange = \"\";\n      }\n    };\n  }, [items]);\n\n  // Track the pointer position relative to the container with one listener.\n  useEffect(() => {\n    if (isEffectDisabled) {\n      pointerRef.current = null;\n      return;\n    }\n    const container = containerRef.current;\n    if (!container) {\n      return;\n    }\n\n    const handlePointerMove = (event: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      pointerRef.current = {\n        x: event.clientX - rect.left,\n        y: event.clientY - rect.top,\n      };\n    };\n    const handlePointerLeave = () => {\n      pointerRef.current = null;\n    };\n\n    container.addEventListener(\"pointermove\", handlePointerMove, {\n      passive: true,\n    });\n    container.addEventListener(\"pointerleave\", handlePointerLeave, {\n      passive: true,\n    });\n\n    return () => {\n      container.removeEventListener(\"pointermove\", handlePointerMove);\n      container.removeEventListener(\"pointerleave\", handlePointerLeave);\n    };\n  }, [isEffectDisabled]);\n\n  // Single rAF loop driving a manual spring integrator per participant.\n  useEffect(() => {\n    if (isEffectDisabled) {\n      for (const state of participantsRef.current.values()) {\n        resetParticipant(state);\n      }\n      for (const [el, state] of elementsRef.current) {\n        resetParticipant(state);\n        el.style.transform = \"translate3d(0px, 0px, 0) rotate(0deg)\";\n      }\n      return;\n    }\n\n    const applyPhysics = (state: ParticipantState, dt: number) => {\n      const pointer = pointerRef.current;\n      let targetX = 0;\n      let targetY = 0;\n      let targetRotate = 0;\n\n      if (pointer) {\n        const dx = pointer.x - state.centerX;\n        const dy = pointer.y - state.centerY;\n        const distance = Math.hypot(dx, dy);\n        const falloff = computeFalloff(distance, radius);\n        if (falloff > 0) {\n          const offset = computeOffset(\n            dx,\n            dy,\n            distance,\n            falloff,\n            strength,\n            mode\n          );\n          targetX = offset.x;\n          targetY = offset.y;\n          if (rotate) {\n            targetRotate = clamp(\n              ((Math.atan2(dy, dx) * 180) / Math.PI) * falloff,\n              -MAX_ROTATE_DEG,\n              MAX_ROTATE_DEG\n            );\n          }\n        }\n      }\n\n      const currentX = state.x.get();\n      const currentY = state.y.get();\n      const currentRotate = state.rotate.get();\n\n      state.vx += ((targetX - currentX) * stiffness - state.vx * damping) * dt;\n      state.vy += ((targetY - currentY) * stiffness - state.vy * damping) * dt;\n      state.vr +=\n        ((targetRotate - currentRotate) * stiffness - state.vr * damping) * dt;\n\n      state.x.set(currentX + state.vx * dt);\n      state.y.set(currentY + state.vy * dt);\n      state.rotate.set(currentRotate + state.vr * dt);\n    };\n\n    const step = (time: number) => {\n      const last = lastFrameRef.current ?? time;\n      const dt = clamp((time - last) / 1000, 0, MAX_DELTA_SECONDS);\n      lastFrameRef.current = time;\n\n      for (const state of participantsRef.current.values()) {\n        applyPhysics(state, dt);\n      }\n      for (const [el, state] of elementsRef.current) {\n        applyPhysics(state, dt);\n        const x = state.x.get();\n        const y = state.y.get();\n        const r = state.rotate.get();\n        el.style.transform = `translate3d(${x}px, ${y}px, 0) rotate(${r}deg)`;\n      }\n\n      rafRef.current = requestAnimationFrame(step);\n    };\n\n    rafRef.current = requestAnimationFrame(step);\n\n    return () => {\n      if (rafRef.current) {\n        cancelAnimationFrame(rafRef.current);\n      }\n      lastFrameRef.current = null;\n    };\n  }, [damping, isEffectDisabled, mode, radius, rotate, stiffness, strength]);\n\n  const content = items\n    ? items.map((item) => {\n        const state = getOrCreateParticipant(item.id);\n        return (\n          <motion.div\n            className=\"inline-block will-change-transform\"\n            key={item.id}\n            ref={(node: HTMLDivElement | null) => {\n              if (node) {\n                nodeMapRef.current.set(item.id, node);\n              } else {\n                nodeMapRef.current.delete(item.id);\n              }\n            }}\n            style={{ rotate: state.rotate, x: state.x, y: state.y }}\n          >\n            {item.element}\n          </motion.div>\n        );\n      })\n    : children;\n\n  return (\n    <div className={cn(\"relative\", className)} ref={containerRef}>\n      {content}\n    </div>\n  );\n};\n\nexport default MagneticField;\n","path":"index.tsx","target":"components/smoothui/magnetic-field/index.tsx","type":"registry:ui"}],"name":"magnetic-field","registryDependencies":[],"title":"Magnetic Field","type":"registry:ui"}