---
title: Magnetic
description: Cursor pulls the control toward it, then snaps back.
group: Pointer
order: 18
new: true
---

Wrap a control and it leans toward the pointer, then settles back when you leave. Tune the pull strength and the field size.

## Button

```tsx
import { Magnetic } from "@/components/motion/magnetic"
import { Button } from "@/components/ui/button"

export function Example() {
  return (
    <Magnetic>
      <Button>Get started</Button>
    </Magnetic>
  )
}
```

## Icon

```tsx
import { Magnetic } from "@/components/motion/magnetic"
import { Button } from "@/components/ui/button"

export function Example() {
  return (
    <Magnetic>
      <Button>Get started</Button>
    </Magnetic>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add magnetic
```

## Usage

```tsx
import { Magnetic } from "@/components/motion/magnetic"
import { Button } from "@/components/ui/button"
```

```tsx
<Magnetic>
  <Button>Get started</Button>
</Magnetic>
```

## API Reference

### Magnetic

| Prop | Type | Default |
| --- | --- | --- |
| strength | number | 0.35 |
| range | number | 40 |
| className | string | — |
| children | ReactNode | — |

Also accepts the other native `div` attributes.

## Source

```tsx
"use client";

import "@/lib/motion-styles";

import {
  useRef,
  useState,
  type ComponentProps,
  type CSSProperties,
  type ReactNode,
} from "react";
import { cn } from "@/lib/utils";

export type MagneticProps = ComponentProps<"div"> & {
  /** How hard the target follows the pointer (0–1). */
  strength?: number;
  /** Extra field around the target in px. */
  range?: number;
  children?: ReactNode;
};

function Magnetic({
  className,
  strength = 0.35,
  range = 40,
  children,
  style,
  onMouseMove,
  onMouseLeave,
  ...props
}: MagneticProps) {
  const fieldRef = useRef<HTMLDivElement>(null);
  const [offset, setOffset] = useState({ x: 0, y: 0 });
  const [active, setActive] = useState(false);
  const pull = Math.min(Math.max(strength, 0), 1);
  const field = Math.max(range, 0);

  function handleMove(clientX: number, clientY: number) {
    const fieldEl = fieldRef.current;
    if (!fieldEl) return;
    if (
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    ) {
      return;
    }

    const rect = fieldEl.getBoundingClientRect();
    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;
    setOffset({
      x: (clientX - centerX) * pull,
      y: (clientY - centerY) * pull,
    });
    setActive(true);
  }

  function handleLeave() {
    setOffset({ x: 0, y: 0 });
    setActive(false);
  }

  return (
    <div
      ref={fieldRef}
      data-slot="magnetic"
      className={cn("relative inline-flex", className)}
      style={
        {
          padding: field,
          margin: -field,
          ...style,
        } as CSSProperties
      }
      onMouseMove={(event) => {
        onMouseMove?.(event);
        handleMove(event.clientX, event.clientY);
      }}
      onMouseLeave={(event) => {
        onMouseLeave?.(event);
        handleLeave();
      }}
      {...props}
    >
      <div
        className="arctis-magnetic-target will-change-transform"
        data-active={active ? "true" : "false"}
        style={
          {
            transform: `translate3d(${offset.x}px, ${offset.y}px, 0)`,
          } as CSSProperties
        }
      >
        {children}
      </div>
    </div>
  );
}

export { Magnetic };
```
