# Magnetic Dropzone

> A dropzone whose target leans towards the pointer and follows dragged files.

A shadcn/ui block from the [@uploadcn](https://uploadcn.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uploadcn/magnetic-dropzone).

- Page, with a live preview: https://shadcn.exchange/r/uploadcn/magnetic-dropzone
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uploadcn/magnetic-dropzone-light-846c5a48fd05e311.png
- Registry item: https://uploadcn.dev/r/new-york-v4/magnetic-dropzone.json
- Categories: upload, animated
- Listed: 2026-10-14, source last changed 2026-10-14

## Install

```bash
npx shadcn@latest add https://uploadcn.dev/r/new-york-v4/magnetic-dropzone.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@uploadcn/core`, `@uploadcn/react`, `cn`, `motion`
- Registry items: `@uploadcn/upload`

## Files

### `registry/default/blocks/magnetic-dropzone.tsx`

```tsx
"use client"

import * as React from "react"
import {
  type UploadAdapter,
  type Uploader,
  type UploadItem as UploadItemData,
  type ValidationOptions,
} from "@uploadcn/core"
import { type UploadRootProps } from "@uploadcn/react"
import { cn } from "cn"
import {
  motion,
  useMotionValue,
  useReducedMotion,
  useSpring,
} from "motion/react"

import { IconPlaceholder } from "@/components/icon-placeholder"
import {
  Upload,
  UploadDropzone,
  UploadDropzoneDescription,
  UploadDropzoneTitle,
  UploadQueue,
} from "@/registry/default/ui/upload"

interface MagneticDropzoneProps<TResult> extends Pick<
  ValidationOptions,
  "accept" | "maxSize" | "maxFiles"
> {
  adapter?: UploadAdapter<TResult>
  /** Use an uploader you created with `useUploader` instead of options. */
  uploader?: Uploader<TResult>
  /** How strongly the target follows the pointer, 0–1. Default `0.35`. */
  strength?: number
  showQueue?: boolean
  onSuccess?: (item: UploadItemData<TResult>) => void
  className?: string
}

/**
 * A dropzone whose target is magnetic: it leans towards the pointer on
 * hover and follows dragged files, then snaps back with a spring.
 */
function MagneticDropzone<TResult = unknown>({
  adapter,
  uploader,
  strength = 0.35,
  showQueue = true,
  className,
  ...options
}: MagneticDropzoneProps<TResult>) {
  const reduceMotion = useReducedMotion()
  const x = useMotionValue(0)
  const y = useMotionValue(0)
  const springX = useSpring(x, { stiffness: 220, damping: 18, mass: 0.6 })
  const springY = useSpring(y, { stiffness: 220, damping: 18, mass: 0.6 })

  const follow = (event: React.PointerEvent | React.DragEvent) => {
    if (reduceMotion) return
    const rect = event.currentTarget.getBoundingClientRect()
    x.set((event.clientX - rect.left - rect.width / 2) * strength)
    y.set((event.clientY - rect.top - rect.height / 2) * strength)
  }
  const reset = () => {
    x.set(0)
    y.set(0)
  }

  return (
    <Upload
      {...((uploader
        ? { uploader }
        : {
            adapter,
            ...options,
          }) as UploadRootProps<TResult>)}
      className={className}
    >
      <UploadDropzone
        variant="muted"
        size="lg"
        onPointerMove={follow}
        onPointerLeave={reset}
        onDragOver={follow}
        onDragLeave={reset}
        onDrop={reset}
        className="overflow-hidden"
      >
        <motion.div
          aria-hidden
          style={{ x: springX, y: springY }}
          className="relative flex size-16 items-center justify-center"
        >
          <span className="absolute inset-0 rounded-2xl border bg-background shadow-sm transition-[transform,border-radius,box-shadow] duration-300 group-data-dragging/upload-dropzone:scale-125 group-data-dragging/upload-dropzone:rounded-full group-data-dragging/upload-dropzone:border-primary group-data-dragging/upload-dropzone:shadow-lg group-data-dragging/upload-dropzone:shadow-primary/20" />
          <span className="absolute inset-0 -z-10 rounded-full bg-primary/15 opacity-0 blur-xl transition-opacity group-data-dragging/upload-dropzone:opacity-100" />
          <IconPlaceholder
            lucide="UploadIcon"
            tabler="IconUpload"
            hugeicons="Upload01Icon"
            phosphor="UploadSimpleIcon"
            remixicon="RiUploadLine"
            className="relative size-5 transition-all duration-200 group-data-dragging/upload-dropzone:scale-0 group-data-dragging/upload-dropzone:opacity-0"
          />
          <IconPlaceholder
            lucide="ArrowDownIcon"
            tabler="IconArrowDown"
            hugeicons="ArrowDownIcon"
            phosphor="ArrowDownIcon"
            remixicon="RiArrowDownLine"
            className="absolute size-5 scale-0 text-primary opacity-0 transition-all duration-200 group-data-dragging/upload-dropzone:scale-100 group-data-dragging/upload-dropzone:opacity-100"
          />
        </motion.div>
        <div className={cn("flex flex-col items-center gap-1")}>
          <UploadDropzoneTitle>
            <span className="group-data-dragging/upload-dropzone:hidden">
              Drag files here
            </span>
            <span className="hidden group-data-dragging/upload-dropzone:inline">
              Let go to upload
            </span>
          </UploadDropzoneTitle>
          <UploadDropzoneDescription>
            or click anywhere to browse
          </UploadDropzoneDescription>
        </div>
      </UploadDropzone>
      {showQueue ? <UploadQueue size="sm" /> : null}
    </Upload>
  )
}

export { MagneticDropzone, type MagneticDropzoneProps }
```
