Atelier UI®

DocsCatalogShader StudioPricingGithub
Docs 1.0.0

tools

  • Browse Catalog
  • Shader Studio
    pro
  • Collage
    new

Documentation

  • Installation
  • How to contribute

Components (45)

  • Orbit Gallery
  • Sphere Gallery
  • Glowing Fog
  • Gradient Flow
  • Halftone Glow
  • Scattered Grid
  • Tag Cloud
  • Edge Bounce
  • Fluid Distortion
  • Image Trail
  • Lens Media
  • Liquid Media
  • Magnetic Dot Grid
  • Pixel Media
  • Pixel Trail
  • Dither Cursor
  • Hover Burst
  • Image Bloom
  • Curve Media
  • Infinite Gallery
  • Infinite Parallax
  • Infinite Zoom
  • Pixel Scroll
  • Scattered Scroll
  • Elastic Stick
  • Letter Swarm
  • Magnify Trail
  • Stacking Grid
  • Wavy Scroll
  • Pixelated Text
  • Text Bounce
  • Text Fluid
  • Text Scramble
  • Falling Text
  • Text Roll
  • Clip Reveal
  • Stripe Wipe
  • Sweep Exit
  • Smooth Scroll
  • Text Split
  • WebGL Image
  • WebGL Provider
  • WebGL Scene
  • WebGL Text
  • WebGL Video
Atelier UI 1.0.0 ©2026
Star on githubBuy me a coffeellms.txt
  1. Docs
  2. /
  3. Components
  4. /
  5. Edge Bounce

Edge Bounce

A wrapper that bounces away from the cursor on hover and springs back into place.

Motion
Tailwind CSS
https://atelier-ui.com/edge-bounce

Settings

pause
0.00
out-duration
0.35
in-duration
1.00
bounce
0.30
distance
35
rotation
25
See the documentation below for more options.

Install

npx atelier-ui add edge-bounce
npm install motion
edge-bounce.tsx
import { useAnimate, useMotionValue, useVelocity } from "motion/react"
import type React from "react"
import { type ComponentRef, type ReactNode, useEffect, useRef } from "react"

const MAX_BOUNCE_SPEED = 1500

export type EdgeBounceProps = {
    children: ReactNode
    className?: string
    pause?: number
    outDuration?: number
    inDuration?: number
    bounce?: number
    distance?: number
    rotation?: number
}

export function EdgeBounce({
    children,
    className,
    pause = 0,
    outDuration = 0.35,
    inDuration = 1,
    bounce = 0.3,
    distance = 35,
    rotation = 25,
}: EdgeBounceProps) {
    const [scope, animate] = useAnimate<ComponentRef<"div">>()
    const isBouncing = useRef(false)
    const touchInside = useRef(false)
    const pointerX = useMotionValue(0)
    const pointerY = useMotionValue(0)
    const velocityX = useVelocity(pointerX)
    const velocityY = useVelocity(pointerY)

    const bounceFrom = async (fromX: number, fromY: number) => {
        const element = scope.current
        if (!element || isBouncing.current) return

        const velX = velocityX.get()
        const velY = velocityY.get()
        const speed = Math.hypot(velX, velY)
        const speedRatio = Math.min(speed / MAX_BOUNCE_SPEED, 1)

        const rect = element.getBoundingClientRect()
        const deltaX = rect.left + rect.width / 2 - fromX
        const deltaY = rect.top + rect.height / 2 - fromY
        const distanceFromCenter = Math.hypot(deltaX, deltaY)

        if (distanceFromCenter === 0 || speed === 0) return

        const tilt = (deltaY * velX - deltaX * velY) / (distanceFromCenter * speed)

        isBouncing.current = true
        await animate(
            element,
            {
                x: (deltaX / distanceFromCenter) * distance * speedRatio,
                y: (deltaY / distanceFromCenter) * distance * speedRatio,
                rotate: tilt * rotation * speedRatio,
            },
            { duration: outDuration, ease: "circOut" },
        )
        isBouncing.current = false

        animate(
            element,
            { x: 0, y: 0, rotate: 0 },
            { type: "spring", duration: inDuration, bounce, delay: pause },
        )
    }

    useEffect(() => {
        const isOver = (event: PointerEvent) => {
            const rect = scope.current?.getBoundingClientRect()
            if (!rect) return false

            return (
                event.clientX >= rect.left &&
                event.clientX <= rect.right &&
                event.clientY >= rect.top &&
                event.clientY <= rect.bottom
            )
        }

        const handlePointerDown = (event: PointerEvent) => {
            if (event.pointerType === "touch") touchInside.current = isOver(event)
        }

        const handlePointerMove = (event: PointerEvent) => {
            pointerX.set(event.clientX)
            pointerY.set(event.clientY)

            if (event.pointerType !== "touch") return

            const inside = isOver(event)
            if (inside && !touchInside.current) bounceFrom(event.clientX, event.clientY)
            touchInside.current = inside
        }

        window.addEventListener("pointerdown", handlePointerDown)
        window.addEventListener("pointermove", handlePointerMove)

        return () => {
            window.removeEventListener("pointerdown", handlePointerDown)
            window.removeEventListener("pointermove", handlePointerMove)
        }
    })

    const handlePointerEnter = (event: React.PointerEvent<HTMLDivElement>) => {
        if (event.pointerType !== "touch") bounceFrom(event.clientX, event.clientY)
    }

    return (
        <div
            ref={scope}
            className={`table will-change-transform ${className ?? ""}`}
            onPointerEnter={handlePointerEnter}
        >
            {children}
        </div>
    )
}
prompt.md
## Integrate the <EdgeBounce /> component from Atelier UI

You are helping integrate an open-source React component into an existing application.

### Component: EdgeBounce
### Description: A wrapper that bounces away from the cursor on hover and springs back into place.
### Dependencies: motion

---

### Usage Example

Wrap the content that should bounce away from the cursor.

```tsx
<EdgeBounce>
    <img src="https://picsum.photos/seed/atelier-1/1200/800" alt="Photo" />
</EdgeBounce>
```

---

### Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | Content to render inside the wrapper. Required. |
| `className` | `string` | — | Class names applied to the wrapper. |
| `pause` | `number` | `0` | Delay in seconds before springing back. |
| `outDuration` | `number` | `0.35` | Duration in seconds for the wrapper moving away. |
| `inDuration` | `number` | `1` | Duration in seconds for the spring-back animation. |
| `bounce` | `number` | `0.3` | Bounciness of the spring-back. `0` = no overshoot, `1` = maximum oscillation. |
| `distance` | `number` | `35` | Maximum pixels the wrapper moves from its origin, reached at full pointer speed. |
| `rotation` | `number` | `25` | Maximum rotation in degrees, following the pointer's travel direction. `0` disables it. |

---

### Full Component Source

#### src/components/edge-bounce/edge-bounce.tsx

```tsx
import { useAnimate, useMotionValue, useVelocity } from "motion/react"
import type React from "react"
import { type ComponentRef, type ReactNode, useEffect, useRef } from "react"

const MAX_BOUNCE_SPEED = 1500

export type EdgeBounceProps = {
    children: ReactNode
    className?: string
    pause?: number
    outDuration?: number
    inDuration?: number
    bounce?: number
    distance?: number
    rotation?: number
}

export function EdgeBounce({
    children,
    className,
    pause = 0,
    outDuration = 0.35,
    inDuration = 1,
    bounce = 0.3,
    distance = 35,
    rotation = 25,
}: EdgeBounceProps) {
    const [scope, animate] = useAnimate<ComponentRef<"div">>()
    const isBouncing = useRef(false)
    const touchInside = useRef(false)
    const pointerX = useMotionValue(0)
    const pointerY = useMotionValue(0)
    const velocityX = useVelocity(pointerX)
    const velocityY = useVelocity(pointerY)

    const bounceFrom = async (fromX: number, fromY: number) => {
        const element = scope.current
        if (!element || isBouncing.current) return

        const velX = velocityX.get()
        const velY = velocityY.get()
        const speed = Math.hypot(velX, velY)
        const speedRatio = Math.min(speed / MAX_BOUNCE_SPEED, 1)

        const rect = element.getBoundingClientRect()
        const deltaX = rect.left + rect.width / 2 - fromX
        const deltaY = rect.top + rect.height / 2 - fromY
        const distanceFromCenter = Math.hypot(deltaX, deltaY)

        if (distanceFromCenter === 0 || speed === 0) return

        const tilt = (deltaY * velX - deltaX * velY) / (distanceFromCenter * speed)

        isBouncing.current = true
        await animate(
            element,
            {
                x: (deltaX / distanceFromCenter) * distance * speedRatio,
                y: (deltaY / distanceFromCenter) * distance * speedRatio,
                rotate: tilt * rotation * speedRatio,
            },
            { duration: outDuration, ease: "circOut" },
        )
        isBouncing.current = false

        animate(
            element,
            { x: 0, y: 0, rotate: 0 },
            { type: "spring", duration: inDuration, bounce, delay: pause },
        )
    }

    useEffect(() => {
        const isOver = (event: PointerEvent) => {
            const rect = scope.current?.getBoundingClientRect()
            if (!rect) return false

            return (
                event.clientX >= rect.left &&
                event.clientX <= rect.right &&
                event.clientY >= rect.top &&
                event.clientY <= rect.bottom
            )
        }

        const handlePointerDown = (event: PointerEvent) => {
            if (event.pointerType === "touch") touchInside.current = isOver(event)
        }

        const handlePointerMove = (event: PointerEvent) => {
            pointerX.set(event.clientX)
            pointerY.set(event.clientY)

            if (event.pointerType !== "touch") return

            const inside = isOver(event)
            if (inside && !touchInside.current) bounceFrom(event.clientX, event.clientY)
            touchInside.current = inside
        }

        window.addEventListener("pointerdown", handlePointerDown)
        window.addEventListener("pointermove", handlePointerMove)

        return () => {
            window.removeEventListener("pointerdown", handlePointerDown)
            window.removeEventListener("pointermove", handlePointerMove)
        }
    })

    const handlePointerEnter = (event: React.PointerEvent<HTMLDivElement>) => {
        if (event.pointerType !== "touch") bounceFrom(event.clientX, event.clientY)
    }

    return (
        <div
            ref={scope}
            className={`table will-change-transform ${className ?? ""}`}
            onPointerEnter={handlePointerEnter}
        >
            {children}
        </div>
    )
}
```

---

### Integration Instructions

1. If you can execute shell commands, run `npx atelier-ui add edge-bounce` from the project root instead of steps 2-3 (it installs everything automatically).
2. Install the npm dependencies: motion.
3. Copy each file from the component source above to the exact path shown.
4. Render `<EdgeBounce />` where it belongs in the app, using the usage example as a starting point and the props table to adjust it.

Full documentation: https://atelier-ui.com/en/docs/components/cursor/edge-bounce

Usage

Wrap the content that should bounce away from the cursor.

<EdgeBounce>
    <img src="https://picsum.photos/seed/atelier-1/1200/800" alt="Photo" />
</EdgeBounce>

API

NameTypeDefaultDescription
childrenReactNode—Content to render inside the wrapper. Required.
classNamestring—Class names applied to the wrapper.
pausenumber0Delay in seconds before springing back.
outDurationnumber0.35Duration in seconds for the wrapper moving away.
inDurationnumber1Duration in seconds for the spring-back animation.
bouncenumber0.3Bounciness of the spring-back. 0 = no overshoot, 1 = maximum oscillation.
distancenumber35Maximum pixels the wrapper moves from its origin, reached at full pointer speed.
rotationnumber25Maximum rotation in degrees, following the pointer's travel direction. 0 disables it.
  • Install
  • Usage
  • API
Star on githubBuy me a coffeellms.txt