# AI Blob Warp

> Animated AI assistant avatar for React and shadcn/ui, a circle filled with a Paper Design Warp shader that pauses for reduced motion.

Source: https://www.cult-ui.com/docs/components/ai-blob-warp

## Example

```tsx title="ai-blob-warp-demo.tsx"
"use client"

import type { ReactNode } from "react"

import { AiBlobWarpAvatar } from "@/components/ui/ai-blob-warp"

function SampleCard({
  label,
  description,
  children,
}: {
  label: string
  description: string
  children: ReactNode
}) {
  return (
    <div className="flex flex-col items-center gap-3 rounded-xl border border-border/60 bg-card/40 p-4 text-center shadow-sm">
      {children}
      <div className="space-y-1">
        <p className="font-medium text-foreground text-sm">{label}</p>
        <p className="max-w-56 text-balance text-muted-foreground text-xs leading-relaxed">
          {description}
        </p>
      </div>
    </div>
  )
}

export default function AiBlobWarpDemo() {
  return (
    <div className="mx-auto max-w-lg space-y-8 px-4 py-10 sm:py-14">
      <div className="text-center">
        <h1 className="mb-2 font-semibold text-2xl text-foreground">
          AI blob warp
        </h1>
        <p className="text-balance text-muted-foreground text-sm leading-relaxed">
          Circular avatar with Paper&apos;s{" "}
          <code className="text-foreground">Warp</code> shader. Size via{" "}
          <code className="text-foreground">className</code>, tuning via{" "}
          <code className="text-foreground">warpProps</code>.
        </p>
      </div>

      <div className="flex flex-wrap justify-center gap-6">
        <SampleCard
          description="Defaults: size-8, Cult-style gradient stops."
          label="Default"
        >
          <AiBlobWarpAvatar aria-label="Default warp avatar" />
        </SampleCard>
        <SampleCard
          description="Larger frame and warm palette override."
          label="Warm"
        >
          <AiBlobWarpAvatar
            aria-label="Warm warp avatar"
            className="size-14"
            warpProps={{
              colors: ["#fda4af", "#fb923c", "#f472b6", "#fecdd3"],
            }}
          />
        </SampleCard>
      </div>
    </div>
  )
}
```

AI Blob Warp is a React avatar component for shadcn/ui built on @paper-design/shaders-react. Use it as the avatar for an AI assistant in chat interfaces or voice agents, and the shader pauses off-screen or when reduced motion is preferred.

## Installation

### CLI

```bash
npx shadcn@latest add @cult-ui/ai-blob-warp
```

### Manual

**Install the required dependencies:**

```bash
npm install @paper-design/shaders-react motion
```

**Copy and paste the following code into your project.**

```tsx title="ai-blob-warp.tsx"
"use client"

import { forwardRef, useCallback, useRef, type ComponentProps } from "react"
import { Warp, type WarpProps } from "@paper-design/shaders-react"
import { motion, useInView, useReducedMotion } from "motion/react"

import { cn } from "@/lib/utils"

const DEFAULT_WARP: Partial<WarpProps> = {
  /** Same stops as the main glow in `halo-search.tsx` (90deg linear-gradient). */
  colors: ["#ff0080", "#7928ca", "#00d4ff", "#0070f3"],
  distortion: 0.25,
  height: 720,
  proportion: 0.54,
  scale: 0.2,
  shape: "checks",
  shapeScale: 1,
  softness: 1,
  speed: 1,
  swirl: 0.8,
  swirlIterations: 10,
  width: 1280,
}

export type AiBlobWarpAvatarProps = Omit<ComponentProps<"div">, "children"> & {
  /** Props forwarded to the Paper `Warp` shader (merged after defaults). */
  warpProps?: Partial<WarpProps>
}

/**
 * Circular avatar frame with an animated Warp shader inside.
 * Motion uses transforms only; shader speed pauses off-screen and when `prefers-reduced-motion` is set.
 */
export const AiBlobWarpAvatar = forwardRef<
  HTMLDivElement,
  AiBlobWarpAvatarProps
>(function AiBlobWarpAvatarImpl(
  { className, warpProps, ...props },
  forwardedRef
) {
  const {
    className: warpClassName,
    speed: warpSpeed,
    ...restWarpProps
  } = warpProps ?? {}
  const rootRef = useRef<HTMLDivElement>(null)
  const reduceMotion = useReducedMotion()
  const inView = useInView(rootRef, {
    amount: 0.2,
    margin: "0px 0px -10% 0px",
  })
  const live = inView && !reduceMotion

  const setRefs = useCallback(
    (node: HTMLDivElement | null) => {
      rootRef.current = node
      if (typeof forwardedRef === "function") {
        forwardedRef(node)
      } else if (forwardedRef) {
        forwardedRef.current = node
      }
    },
    [forwardedRef]
  )

  const baseSpeed = warpSpeed ?? DEFAULT_WARP.speed ?? 1

  return (
    <div
      className={cn(
        "relative isolate size-8 overflow-hidden rounded-full border border-border/10 bg-muted",
        className
      )}
      ref={setRefs}
      {...props}
    >
      <motion.div
        animate={
          live
            ? {
                scale: [1, 1.055, 0.99, 1],
                rotate: [0, 2, -1.5, 0],
              }
            : { scale: 1, rotate: 0 }
        }
        aria-hidden
        className="pointer-events-none absolute inset-0 origin-center rounded-full"
        transition={{
          duration: 5.2,
          ease: "easeInOut",
          repeat: Number.POSITIVE_INFINITY,
        }}
      >
        <Warp
          {...DEFAULT_WARP}
          {...restWarpProps}
          speed={live ? baseSpeed : 0}
        />
      </motion.div>
    </div>
  )
})

AiBlobWarpAvatar.displayName = "AiBlobWarpAvatar"
```

**Update the import paths to match your project setup.**
