# Cullet — Animated UI Component for React

> Liquid-glass surface primitive: backdrop blur plus an SVG feDisplacementMap generated from the element's own size — edge-weighted refraction with a per-channel prismatic split, a cyan/magenta chromatic rim, a pointer-leaning specular highlight and a surface-tension wobble. Render it as a pill, rounded tile, circle, button or link; CulletLens is a pointer-following magnifier with idle drift and keyboard walking. Refraction renders in Chromium; Safari and Firefox fall back to frosted blur + rim.

- Page: https://crucibleui.com/components/utility/cullet
- Category: [utility](https://crucibleui.com/components/utility)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: css

## Install

```bash
npx shadcn@latest add @crucible/cullet
```

Pro install requires adding the token-bearing `@crucible` registry to components.json (issued at https://crucibleui.com/account after purchase).

Import from `@/components/crucible/cullet`.

## Props (`CulletProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children?` | `React.ReactNode` | — | Content floated on the glass — a label, an icon, a whole card. |
| `as?` | `"div" \| "button" \| "a" \| "span" \| "section" \| "article" \| "figure" \| "li"` | `"div"` | Element to render. Keyboard behaviour and roles come from it: `"button"` is a native button, `"a"` a link (pass `href`). |
| `shape?` | `CulletShape` | `"pill"` | Silhouette: fully rounded pill, a soft rounded rect, or a circle (forced 1:1). |
| `strength?` | `number` | `1` | Refraction strength multiplier — how far the rim bends what is behind it. 0 disables bending. |
| `blur?` | `number` | `2` | Backdrop blur, in px. Liquid glass stays mostly clear; raise for frost. |
| `tint?` | `string` | `"rgba(255,255,255,0.06)"` | Fill colour laid over the refracted backdrop (any CSS colour; keep alpha low). |
| `chromatic?` | `number` | `1` | Chromatic rim + channel-split intensity, 0–2. |
| `bezel?` | `number` | `0.6` | Width of the refracting bezel as a fraction of the shorter half-side, 0.1–1. |
| `wobble?` | `boolean` | `true` | Surface-tension wobble on hover, press, pointer motion and resize. Ignored under reduced motion. |
| `refraction?` | `CulletRefraction` | `"auto"` | SVG displacement mode. `"auto"` enables it only where it renders (Blink); Safari and Firefox fall back to blur + rim. |
| `href?` | `string` | — | Link target when `as="a"`. |
| `target?` | `string` | — | Link target window when `as="a"`. |
| `rel?` | `string` | — | Link rel when `as="a"`. |
| `type?` | `"button" \| "submit" \| "reset"` | `"button"` | Button type when `as="button"`. |
| `disabled?` | `boolean` | — | Disables the button when `as="button"`. |
| `ref?` | `React.Ref<CulletHandle>` | — | Imperative handle — see `CulletHandle`. |

Also accepts: `Omit<React.HTMLAttributes<HTMLElement>, "children">`.

## Usage example

```tsx
"use client";

import { Cullet, CulletLens } from "@/components/crucible/cullet";

const WORDS = ["refract", "bend", "light", "surface", "glass", "edge", "lens", "clear"];

/** A busy, deterministic backdrop: soft colour fields, hairline stripes and rows of type. */
function BusyField() {
  return (
    <div aria-hidden className="absolute inset-0 overflow-hidden bg-neutral-950">
      <div
        className="absolute inset-0"
        style={{
          background:
            "radial-gradient(38% 50% at 18% 30%, #3b82f6 0%, transparent 70%), radial-gradient(34% 46% at 82% 26%, #ec4899 0%, transparent 70%), radial-gradient(40% 50% at 60% 92%, #14b8a6 0%, transparent 72%), radial-gradient(30% 40% at 40% 60%, #f59e0b 0%, transparent 70%)",
          opacity: 0.75,
        }}
      />
      <div
        className="absolute inset-0 opacity-[0.22]"
        style={{
          backgroundImage:
            "repeating-linear-gradient(90deg, rgba(255,255,255,0.9) 0 1px, transparent 1px 22px), repeating-linear-gradient(0deg, rgba(255,255,255,0.6) 0 1px, transparent 1px 22px)",
        }}
      />
      <div className="absolute inset-0 flex flex-col justify-center gap-1 font-semibold tracking-tight text-white/80 select-none">
        {Array.from({ length: 9 }).map((_, row) => (
          <div
            key={row}
            className="whitespace-nowrap text-[28px] leading-[1.05]"
            style={{ transform: `translateX(${-((row * 37) % 120)}px)`, opacity: row % 2 ? 0.55 : 0.9 }}
          >
            {Array.from({ length: 8 })
              .map((__, i) => WORDS[(row * 3 + i) % WORDS.length])
              .join(" · ")}
          </div>
        ))}
      </div>
    </div>
  );
}

export default function CulletDemo() {
  return (
    <div className="relative isolate flex h-full min-h-[360px] w-full items-center justify-center overflow-hidden rounded-xl bg-neutral-950">
      <CulletLens size={150} zoom={1.7} restPosition={{ x: 0.82, y: 0.3 }} className="absolute inset-0">
        <BusyField />
        <div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-5">
          <div className="flex flex-wrap items-center justify-center gap-3">
            <Cullet as="button" className="text-sm font-medium">
              Play
            </Cullet>
            <Cullet as="button" className="text-sm font-medium">
              Share
            </Cullet>
            <Cullet as="button" shape="circle" aria-label="Add" className="w-12 text-lg">
              +
            </Cullet>
          </div>
          <Cullet shape="rounded" className="flex-col gap-1 px-7 py-5 text-center">
            <span className="text-base font-semibold tracking-tight">Move over the glass.</span>
            <span className="text-xs text-white/70">Press a pill — it wobbles</span>
          </Cullet>
        </div>
      </CulletLens>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.
