{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "magnetic",
  "type": "registry:component",
  "title": "Magnetic",
  "description": "A cursor-attraction wrapper that makes inner content subtly follow the mouse position.",
  "dependencies": [
    "@radix-ui/react-slot"
  ],
  "registryDependencies": [
    "@joyco/utils"
  ],
  "files": [
    {
      "path": "registry/components/magnetic.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cn } from '@/lib/utils'\nimport { useComposedRefs } from '@/registry/lib/compose-refs'\n\n/* -------------------------------------------------------------------------------------------------\n * Types\n * -----------------------------------------------------------------------------------------------*/\n\ntype MagneticContextValue = {\n  rootRef: React.RefObject<HTMLElement | null>\n  mousePos: React.RefObject<{ x: number; y: number }>\n  strength: number\n  ease: number\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Context\n * -----------------------------------------------------------------------------------------------*/\n\nconst MagneticContext = React.createContext<MagneticContextValue | null>(null)\n\nfunction useMagneticContext() {\n  const context = React.useContext(MagneticContext)\n  if (!context) {\n    throw new Error('Magnetic.Inner must be used within a Magnetic.Root')\n  }\n  return context\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Root\n * -----------------------------------------------------------------------------------------------*/\n\ninterface RootProps extends React.ComponentPropsWithRef<'div'> {\n  asChild?: boolean\n  /** How strongly inner content follows the cursor (0–1) */\n  strength?: number\n  /** Return-to-center transition duration in ms */\n  ease?: number\n}\n\nfunction Root({\n  asChild,\n  ref: forwardedRef,\n  strength = 0.35,\n  ease = 350,\n  children,\n  ...props\n}: RootProps) {\n  const rootRef = React.useRef<HTMLElement | null>(null)\n  const mousePos = React.useRef({ x: 0, y: 0 })\n  const composedRef = useComposedRefs(forwardedRef, rootRef)\n\n  const handleMouseMove = (e: React.MouseEvent) => {\n    mousePos.current = { x: e.pageX, y: e.pageY }\n    props.onMouseMove?.(e as React.MouseEvent<HTMLDivElement>)\n  }\n\n  const handleMouseEnter = (e: React.MouseEvent) => {\n    props.onMouseEnter?.(e as React.MouseEvent<HTMLDivElement>)\n  }\n\n  const handleMouseLeave = (e: React.MouseEvent) => {\n    props.onMouseLeave?.(e as React.MouseEvent<HTMLDivElement>)\n  }\n\n  const contextValue = React.useMemo<MagneticContextValue>(\n    () => ({ rootRef, mousePos, strength, ease }),\n    [strength, ease]\n  )\n\n  const Comp = asChild ? Slot : 'div'\n\n  return (\n    <MagneticContext.Provider value={contextValue}>\n      <Comp\n        ref={composedRef}\n        {...props}\n        onMouseMove={handleMouseMove}\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n      >\n        {children}\n      </Comp>\n    </MagneticContext.Provider>\n  )\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Inner\n * -----------------------------------------------------------------------------------------------*/\n\ninterface InnerProps extends React.ComponentPropsWithRef<'div'> {\n  asChild?: boolean\n}\n\nfunction Inner({\n  asChild,\n  ref: forwardedRef,\n  className,\n  style,\n  ...props\n}: InnerProps) {\n  const { rootRef, mousePos, strength, ease } = useMagneticContext()\n  const innerRef = React.useRef<HTMLElement | null>(null)\n  const composedRef = useComposedRefs(forwardedRef, innerRef)\n  const rafId = React.useRef<number>(0)\n  const current = React.useRef({ x: 0, y: 0 })\n  const isLooping = React.useRef(false)\n  const prefersReducedMotion = React.useRef(false)\n\n  const applyTick = React.useEffectEvent(() => {\n    if (prefersReducedMotion.current) return false\n\n    const el = innerRef.current\n    const rootEl = rootRef.current\n    if (!el || !rootEl) return false\n\n    const rect = rootEl.getBoundingClientRect()\n    const centerX = rect.left + window.scrollX + rect.width / 2\n    const centerY = rect.top + window.scrollY + rect.height / 2\n\n    const lerpFactor = 0.15\n    const settleThreshold = 0.1\n    const targetX = (mousePos.current.x - centerX) * strength\n    const targetY = (mousePos.current.y - centerY) * strength\n\n    current.current.x += (targetX - current.current.x) * lerpFactor\n    current.current.y += (targetY - current.current.y) * lerpFactor\n\n    const settled =\n      Math.abs(targetX - current.current.x) < settleThreshold &&\n      Math.abs(targetY - current.current.y) < settleThreshold\n\n    if (settled) {\n      current.current.x = targetX\n      current.current.y = targetY\n      isLooping.current = false\n    }\n\n    el.style.transform = `translate3d(${current.current.x}px, ${current.current.y}px, 0)`\n\n    return !settled\n  })\n\n  const applyLeave = React.useEffectEvent(() => {\n    cancelAnimationFrame(rafId.current)\n    isLooping.current = false\n\n    if (prefersReducedMotion.current) return\n\n    const el = innerRef.current\n    if (!el) return\n\n    current.current = { x: 0, y: 0 }\n    el.style.transition = `transform ${ease}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`\n    el.style.transform = 'translate3d(0, 0, 0)'\n  })\n\n  React.useEffect(() => {\n    const mq = window.matchMedia('(prefers-reduced-motion: reduce)')\n    prefersReducedMotion.current = mq.matches\n\n    const onChange = (e: MediaQueryListEvent) => {\n      prefersReducedMotion.current = e.matches\n      if (e.matches) {\n        cancelAnimationFrame(rafId.current)\n        const el = innerRef.current\n        if (el) {\n          el.style.transform = 'translate3d(0, 0, 0)'\n          el.style.transition = ''\n        }\n      }\n    }\n\n    mq.addEventListener('change', onChange)\n    return () => mq.removeEventListener('change', onChange)\n  }, [])\n\n  React.useEffect(() => {\n    const root = rootRef.current\n    const inner = innerRef.current\n    if (!root || !inner) return\n\n    function tick() {\n      if (applyTick()) {\n        rafId.current = requestAnimationFrame(tick)\n      }\n    }\n\n    function handleEnter() {\n      if (prefersReducedMotion.current) return\n\n      const el = innerRef.current\n      if (!el) return\n\n      el.style.transition = ''\n      isLooping.current = true\n      rafId.current = requestAnimationFrame(tick)\n    }\n\n    function handleMove() {\n      if (isLooping.current) return\n\n      const el = innerRef.current\n      if (!el || prefersReducedMotion.current) return\n\n      el.style.transition = ''\n      isLooping.current = true\n      rafId.current = requestAnimationFrame(tick)\n    }\n\n    root.addEventListener('mouseenter', handleEnter)\n    root.addEventListener('mousemove', handleMove)\n    root.addEventListener('mouseleave', applyLeave)\n\n    return () => {\n      cancelAnimationFrame(rafId.current)\n      root.removeEventListener('mouseenter', handleEnter)\n      root.removeEventListener('mousemove', handleMove)\n      root.removeEventListener('mouseleave', applyLeave)\n    }\n  }, [rootRef])\n\n  const Comp = asChild ? Slot : 'div'\n\n  return (\n    <Comp\n      ref={composedRef}\n      className={cn('will-change-transform', className)}\n      style={{ ...style, transform: 'translate3d(0, 0, 0)' }}\n      {...props}\n    />\n  )\n}\n\nexport { Root, Inner }\n",
      "type": "registry:component"
    },
    {
      "path": "registry/lib/compose-refs.ts",
      "content": "import * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n  // eslint-disable-next-line react-hooks/use-memo\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "type": "registry:lib"
    }
  ]
}