# Pinch

> For agents: this file is the source. Write the file under Code exactly as given, at the path it names, and mount the component as Placement says; do not recreate or approximate the animation.

A grid of cells, each filled by its square. Near the pointer the squares shrink to the smallest unit in a soft dome, trailing it; they fill their cells again when it leaves. Cells, inverted.

- Slug: `pinch` · Category: grids · Free (MIT) · v0.1.0
- What moves: Every cell is filled by its square. Near the pointer the squares shrink to the smallest unit in a soft dome, then fill their cells again when it leaves.
- Triggers: hover, focus, pointer-move · Touch: ambient (plays by itself on screens without hover, 1.5× pace, rests off screen) · Reduced motion: simplified
- Aliases: cells inverted, pixel grid, cell grid, LED board off, halftone squares · Use cases: hero, background, image tile · Tags: grid, cells, squares, shrink, pinch, follow, soft, dense
- Rails: Measured 2026-10-27 for 0.1.0: 1.34 KB js, p95 9.98 ms on a 4× throttled CPU; not yet within every budget line, so no Light badge
- Estimated tokens: 4863

## Tokens

- `--texture-ink` (color, default `var(--foreground)`): Ink of the marks
- `--texture-field` (color, default `var(--background)`): The panel behind them

## Placement

**Never set width or height on the Texture; size the host. The Texture fills whatever box it is given.** Works from 160 × 113 px up; keeps 1/0.7049 (cover by default; cover, contain); clips. The host needs position: relative.

### contained

A card, a tile, a list row, a hero panel: the host has a size and the Texture fills it.

```css
.host { position: relative; aspect-ratio: 1 / 0.7049; width: 100%; max-width: 48rem; overflow: hidden; border-radius: 4px; }
```

```tsx
<div className="host">
  <Pinch />
  <span className="sr-only">Pinch</span>
</div>
```

### background

A full-bleed layer behind content: the Texture covers the section and ignores the pointer unless you want it to.

```css
.section { position: relative; }
.section > .host { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
.section > .content { position: relative; z-index: 1; }
```

```tsx
<section className="section">
  <div className="host" aria-hidden="true"><Pinch /></div>
  <div className="content">…</div>
</section>
```

## Install

- Command: `npx shadcn@latest add https://textures.dev/r/pinch.json` (one self-contained React file at components/textures/pinch.tsx; Pro through the Copy to Agent prompt's token URL, or `npx shadcn@latest add @textures/pinch` with the @textures registry and its header in components.json, see below)
- MCP: `get_texture({ ref: "pinch" })` (server: /mcp)
- Markdown: /s/pinch.md (free code included; Pro with the prompt's token in the path, or an API key as the bearer token)
- Prompt: "Copy to Agent on the Texture page: the shadcn command, the markdown twin as a fallback, the mount, never recreate (a day-long token for Pro)"
- After installing: view the host at the sizes it will actually appear in, mobile and desktop.
- Suggested commit: `feat: add Pinch hover texture (textures.dev/s/pinch)` (a suggestion the agent and the human can edit, never a write).

## Code

Write these files into the project, then mount the component on a host with position: relative and a size.

### React

### `components/textures/pinch.tsx`

```tsx
// Pinch — a Texture by Textures · https://textures.dev/s/pinch
// Install another: npx shadcn@latest add https://textures.dev/r/<slug>.json
// For agents: https://textures.md
// License: free to use
"use client"
import { type CSSProperties, useEffect, useRef, useState } from "react"
function selfSize(root: HTMLElement | null): void {
	const host = root?.parentElement
	if (!root || !host || host.getBoundingClientRect().height > 0) return
	root.style.minHeight = "240px"
}
const ASPECT = 0.7049
const MIN_BOX: [number, number] = [160, 113]
type El = { id: string; role: string; d: string; cx: number; cy: number; ny?: number }
const PACKED =
	"7d1NSsNQFMXx+8SB6MyhA0k3oVtwN7YuwZG4Mx07EQoNTlxBnQhGUoux8as1JmjyUzAn0Ms77/bl3/OSgukmpdiNSFsvv9s0TdM0TdM0TdM0TdM0TdP0/9Un+5WmKIqiKIqiKIqiKIqiqM/VY9zGYYoYRfmTRSzPZkXE3VMsjvF6lhfl68YpvqzJazXlKyY/qGljnL7NZ0h9y2o1+QDH2aSmyfvT1Th9uX62p5OEo9YBjuIojjbh6BhHrQMcxVEc/TWOjr65fmYfXD+jb8aZfTCf9WraGKdv8xlS37JaTT7AcTapafL+dDVOX64fHLUOcBRHcbQpR8/s6+1L7Ovt6+3rG+3rT+VRn6fyqDwqj8qj8qg8Ko/Ko57X46i+4SiO+v4oHuAojuKoPIqjOIqjOIqj8iiO4iiO4qg8iqP6hqM42oM8mtWun3yN6yerjZOvMZ+va9oYp2/zGVLfslpNPsBxNqlp8v50NU5frh951OepPCqPyqPyqDwqj8qj8qg8Ko/qmzwqj8qjcpU8Ko/Ko0O9fg6m5/cPEXG0qDmOiPnieF1E7BaxOEZcFRF7RXk8WnqbL1/9tqZ61WrN8dLbT2p4402vefvr3namk6TXvOGoXvP2Wxwt/76crdZUZ+/nU9Wseqtq3s9n/RreeNNr3v66NxzlDUf1mremHB3b1/NmX6/XvLk/yhuO6jVvOGod8IajvOGoXvOGo7zhqHvRvHnOZB3w5nk9bziq17zhqHXAG47y5ntP7qHw5v4ob54z6TVvOGod8IajvOGoXvOGo9YBb3rNm+89uRfNm+dMvHler9e84ah1wBuO8oajes1bi94u00Uq60qPbR7L/1/SzThj8zEf8zEf8zGfzufzDA=="
type Geometry = { elements: El[]; plates: string[] }
let pending: Promise<Geometry> | null = null
async function unpack(): Promise<Geometry> {
	const bytes = Uint8Array.from(atob(PACKED), (c) => c.charCodeAt(0))
	const v = new DataView(
		await new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw"))).arrayBuffer(),
	)
	let o = 0
	const u8 = () => v.getUint8(o++)
	const u16 = () => ((o += 2), v.getUint16(o - 2, true))
	const i16 = () => ((o += 2), v.getInt16(o - 2, true))
	u8()
	const ne = u16()
	const ops = Array.from({ length: ne + u16() }, () => Array.from({ length: u16() }, u8))
	let x = 0
	let y = 0
	const pt = () => `${(x += i16()) / 10000} ${(y += i16()) / 10000}`
	const paths = ops.map((a) =>
		a
			.map(
				(c) =>
					["M", "L", "Q", "A", "Z"][c] +
					(c === 2
						? `${pt()} ${pt()}`
						: c === 3
							? `${i16() / 10000} ${i16() / 10000} ${i16() / 10} ${i16()} ${i16()} ${pt()}`
							: c === 4
								? ""
								: pt()),
			)
			.join(""),
	)
	let cx = 0
	let cy = 0
	const elements = paths
		.slice(0, ne)
		.map((d, i): El => ({ id: String(i), role: "motif", d, cx: (cx += i16()) / 10000, cy: (cy += i16()) / 10000 }))
	return { elements, plates: paths.slice(ne) }
}
function useGeometry(): Geometry | null {
	const [geometry, setGeometry] = useState<Geometry | null>(null)
	useEffect(() => {
		let live = true
		void (pending ??= unpack()).then((g) => live && setGeometry(g))
		return () => {
			live = false
		}
	}, [])
	return geometry
}
type React_ = {
	target: string
	property: "translate" | "scale" | "rotate" | "opacity"
	along?: "normal" | "tangent" | "toward" | "away" | "x" | "y"
	max: number
	map?: "falloff" | "inverse-falloff" | "constant"
}
type Spec = {
	ir?: 1
	behavior?: "displace" | "clear" | "glow"
	rest?: "static"
	pointer: {
		falloff: {
			shape?: "radial" | "directional"
			radius?: number
			curve?: Curve
		}
	}
	react: React_[]
	stagger?: { by?: "distance" | "index" | "none"; ms?: number }
	leave: {
		settle: "spring" | "ease"
		spring?: { stiffness: number; damping: number }
	}
	reducedMotion: "none" | "static" | "simplified"
	a11y?: {
		keyboard?: "center-pulse" | "center-hold" | "none"
		touch?: "ambient" | "tap-ripple" | "tap-toggle" | "static"
	}
}
type Target = { el: SVGGraphicsElement; role: string; index: number; cx: number; cy: number }
type Options = { spec: Spec; targets: Target[]; aspect: number; min?: [number, number] }
const AMBIENT_SPEED = 1.5
const AMBIENT_CYCLE = 4.5
const LEAVE_K = 900
const LEAVE_MS = 350
function mount(root: HTMLElement, o: Options): () => void {
	const svg = root.querySelector("svg")
	if (!svg) return () => {}
	const { spec } = o
	const fall = spec.pointer.falloff
	const rad0 = fall.radius ?? 0.35
	const curve = curves[fall.curve ?? "smoothstep"]
	const short = Math.min(1, o.aspect)
	const calm = () => matchMedia("(prefers-reduced-motion: reduce)").matches || matchMedia("(update: slow)").matches
	const reduced = () => spec.reducedMotion !== "none" && calm()
	const n = o.targets.length
	const C: number = 1
	const val = new Float32Array(n * C)
	const vel = new Float32Array(n * C)
	const amp = Math.max(1, ...spec.react.map((r) => Math.abs(r.max)))
	const rules = spec.react.map((r) => ({
		...r,
		idx: o.targets.map((t, i) => (r.target === "all" || t.role === r.target ? i : -1)).filter((i) => i >= 0),
	}))
	const mask = new Uint32Array(n)
	rules.forEach((r, ri) => {
		for (const i of r.idx) mask[i]! |= 1 << ri
	})
	const wrote: string[] = new Array(n).fill("")
	for (const t of o.targets) {
		t.el.style.transformBox = "fill-box"
		t.el.style.transformOrigin = "center"
	}
	let box: DOMRect | null = null
	let px = 0.5
	let py = short / 2
	let act = 0
	let w = 0
	let wv = 0
	let t0 = 0
	let raf = 0
	let last = 0
	let seen = true
	let gone = 0
	const touchQ = matchMedia("(hover: none), (pointer: coarse)")
	const wantsAmb = () => {
		const forced = root.closest("[data-motion]")?.getAttribute("data-motion")
		return (spec.a11y?.touch ?? "ambient") === "ambient" && (forced ? forced === "ambient" : touchQ.matches)
	}
	const wAmb = (2 * Math.PI) / AMBIENT_CYCLE
	const ph = Math.random() * 2 * Math.PI
	let amb = false
	let clock = 0
	let hold = 0
	const fit = () => {
		const r = svg.getBoundingClientRect()
		box = r.width < (o.min?.[0] ?? 0) || r.height < (o.min?.[1] ?? 0) ? null : r
	}
	const toUnit = (x: number, y: number) => {
		if (!box) return
		px = (x - box.left) / box.width
		py = ((y - box.top) / box.height) * o.aspect
	}
	const tick = (now: number) => {
		const dt = Math.min(last ? (now - last) / 1000 : 1 / 60, 1 / 30)
		last = now
		const snap = reduced()
		if (amb && now > hold) {
			if (snap || calm()) act = 0
			else {
				clock += dt * AMBIENT_SPEED
				const t = clock
				px = 0.5 + 0.5 * (0.76 * Math.sin(wAmb * t + ph) + 0.2 * Math.sin(1.93 * wAmb * t + 1.2 + ph))
				py =
					(o.aspect / 2) *
					(1 + 0.72 * Math.sin(0.75 * wAmb * t + 0.8 + ph) + 0.2 * Math.sin(1.37 * wAmb * t + 2.1 + ph))
				if (!act) t0 = now
				gone = 0
				act = 1
			}
		}
		const st = spec.leave.spring ?? { stiffness: 260, damping: 28 }
		let busy = false
		if (snap) w = act
		else if (!act) [w, wv] = spring(w, wv, 0, LEAVE_K, 2 * Math.sqrt(LEAVE_K), dt)
		else [w, wv] = spring(w, wv, act, LEAVE_K, 2 * Math.sqrt(LEAVE_K), dt)
		const z = (v: number) => (Math.abs(v) < 1e-5 ? 0 : v)
		const rad = rad0 * short
		for (let i = 0; i < n; i++) {
			const t = o.targets[i]!
			let dx = px - t.cx
			let dy = py - t.cy
			const dist = Math.sqrt(dx * dx + dy * dy) / rad
			const outside = dist >= 1
			if (outside) {
				if (val[i] === 0 && vel[i] === 0) continue
			}
			const on = act && now - t0 >= 0 ? w : 0
			const f0 = dist >= 1 ? 0 : curve(1 - dist)
			for (let c = 0; c < C; c++) {
				const k = i * C + c
				const goal = act ? f0 * on : 0
				if (snap) val[k] = goal
				else if (!act) {
					const kk = Math.max(st.stiffness, LEAVE_K)
					;[val[k], vel[k]] = spring(val[k]!, vel[k]!, goal, kk, 2 * Math.sqrt(kk), dt) as [number, number]
				} else [val[k], vel[k]] = spring(val[k]!, vel[k]!, goal, st.stiffness, st.damping, dt) as [number, number]
				if (goal * amp < 0.005 && Math.abs(val[k]!) * amp < 0.005) {
					val[k] = 0
					vel[k] = 0
					continue
				}
				if ((Math.abs(val[k]! - goal) + Math.abs(vel[k]!) * dt) * amp > 1e-3) busy = true
				else if (Math.abs(val[k]!) * amp <= 1e-3 && !act) {
					val[k] = 0
					vel[k] = 0
				}
			}
			if (val[i] === 0 && wrote[i] === "") continue
			let sx = 1
			let sy = 1
			const m = mask[i]!
			for (let ri = 0; ri < rules.length; ri++) {
				if (!(m & (1 << ri))) continue
				const r = rules[ri]!
				const ch = 0
				const amt = val[i * C + ch]! * r.max
				if (r.property === "scale") {
					sx += amt
					sy += amt
				}
			}
			const r3 = (v: number) => Math.round(v * 100) / 100
			const ssx = r3(1 + z(sx - 1))
			const ssy = r3(1 + z(sy - 1))
			const next = (ssx !== 1 || ssy !== 1 ? `scale(${ssx},${ssy})` : "") || "translate(0px,0px)"
			if (next !== wrote[i]) {
				t.el.style.transform = next
				wrote[i] = next
			}
		}
		if (Math.abs(w - act) * amp > 1e-3 || Math.abs(wv) * dt * amp > 1e-3) busy = true
		else if (act) {
			w = act
			wv = 0
		}
		if (!act && gone && now - gone > LEAVE_MS) busy = false
		raf = busy || (amb && !snap && !calm()) ? requestAnimationFrame(tick) : 0
		if (!raf) {
			if (act) last = 0
			else rest()
		}
	}
	function rest() {
		last = 0
		w = 0
		wv = 0
		val.fill(0)
		vel.fill(0)
		for (let i = 0; i < n; i++) {
			const t = o.targets[i]!
			if (wrote[i] !== "") t.el.style.transform = ""
			wrote[i] = ""
		}
	}
	const go = () => {
		if (!raf && (seen || hold > performance.now()) && box) raf = requestAnimationFrame(tick)
	}
	const enterAt = (x: number, y: number) => {
		if (timer) {
			clearTimeout(timer)
			timer = 0
		}
		fit()
		if (!box) return
		toUnit(x, y)
		if (!act) t0 = performance.now()
		gone = 0
		act = 1
		go()
	}
	let timer: ReturnType<typeof setTimeout> | 0 = 0
	const letGo = () => {
		if (act) gone = performance.now()
		act = 0
		go()
	}
	const leave = (e?: Event) => {
		if (e && isTouch(e as PointerEvent)) return
		if (e && !e.isTrusted) hold = 0
		if (amb && performance.now() > hold) return
		return letGo()
	}
	const isTouch = (e: PointerEvent) => e.pointerType === "touch" && touchQ.matches
	const onMove = (e: PointerEvent) => {
		if (isTouch(e)) return
		if (!e.isTrusted) hold = performance.now() + 1000
		lastX = e.clientX
		lastY = e.clientY
		if (!box) fit()
		toUnit(e.clientX, e.clientY)
		if (!act) enterAt(e.clientX, e.clientY)
		else go()
	}
	const onIn = (e: PointerEvent) => !isTouch(e) && enterAt(e.clientX, e.clientY)
	const onDown = (e: PointerEvent) => {
		if (e.pointerType !== "touch" || touchQ.matches) return
		enterAt(e.clientX, e.clientY)
		setTimeout(leave, 600)
	}
	const onFoc = () => {
		fit()
		if (!box) return
		px = 0.5
		py = o.aspect / 2
		t0 = performance.now()
		gone = 0
		act = 1
		go()
		setTimeout(leave, 900)
	}
	root.addEventListener("pointerenter", onIn, { passive: true })
	root.addEventListener("pointermove", onMove, { passive: true })
	root.addEventListener("pointerleave", leave)
	root.addEventListener("pointerdown", onDown)
	root.addEventListener("focusin", onFoc)
	root.addEventListener("focusout", leave)
	const ro =
		typeof ResizeObserver !== "undefined"
			? new ResizeObserver(() => {
					fit()
					if (amb) go()
				})
			: null
	let lastX = 0
	let lastY = 0
	const onScr = () => {
		if (!act || amb) return
		fit()
		if (box) toUnit(lastX, lastY)
		go()
	}
	window.addEventListener("scroll", onScr, { passive: true, capture: true })
	ro?.observe(svg)
	const io =
		typeof IntersectionObserver !== "undefined"
			? new IntersectionObserver(
					([e]) => {
						seen = !!e?.isIntersecting
						sync()
						if (!seen && raf && hold < performance.now()) {
							cancelAnimationFrame(raf)
							raf = 0
							last = 0
							rest()
						} else go()
					},
					{ threshold: 0.2 },
				)
			: null
	const sync = () => {
		const next = seen && !document.hidden && wantsAmb()
		if (next === amb) return
		amb = next
		if (amb) {
			fit()
			go()
		} else {
			if (act) gone = performance.now()
			act = 0
			go()
		}
	}
	const onVis = () => sync()
	const reduceQ = matchMedia("(prefers-reduced-motion: reduce)")
	const onReduce = () => go()
	document.addEventListener("visibilitychange", onVis)
	touchQ.addEventListener?.("change", sync)
	reduceQ.addEventListener?.("change", onReduce)
	io?.observe(svg)
	fit()
	if (!io) sync()
	return () => {
		root.removeEventListener("pointerenter", onIn)
		root.removeEventListener("pointermove", onMove)
		root.removeEventListener("pointerleave", leave)
		root.removeEventListener("pointerdown", onDown)
		root.removeEventListener("focusin", onFoc)
		root.removeEventListener("focusout", leave)
		ro?.disconnect()
		io?.disconnect()
		window.removeEventListener("scroll", onScr, { capture: true })
		document.removeEventListener("visibilitychange", onVis)
		touchQ.removeEventListener?.("change", sync)
		reduceQ.removeEventListener?.("change", onReduce)
		if (timer) clearTimeout(timer)
		if (raf) cancelAnimationFrame(raf)
		for (const t of o.targets) {
			t.el.style.transform = ""
		}
	}
}
const curves = {
	smoothstep: (t: number) => t * t * (3 - 2 * t),
} as const
type Curve = keyof typeof curves
function spring(value: number, vel0: number, goal: number, stiff: number, damp: number, dt: number): [number, number] {
	const steps = Math.max(1, Math.ceil(dt * 240))
	const h = dt / steps
	let x = value
	let v = vel0
	for (let n = 0; n < steps; n++) {
		v += (-stiff * (x - goal) - damp * v) * h
		x += v * h
	}
	return [x, v]
}
const MOTION: Spec = {
	ir: 1,
	behavior: "displace",
	pointer: {
		falloff: {
			shape: "radial",
			radius: 0.42,
			curve: "smoothstep",
		},
	},
	react: [
		{
			target: "motif",
			property: "scale",
			max: -0.8462,
			map: "falloff",
		},
	],
	stagger: {
		by: "none",
		ms: 0,
	},
	leave: {
		settle: "spring",
		spring: {
			stiffness: 900,
			damping: 60,
		},
	},
	rest: "static",
	reducedMotion: "simplified",
	a11y: {
		keyboard: "center-pulse",
		touch: "ambient",
	},
}
export type PinchProps = {
	tokens?: Partial<Record<"--texture-ink" | "--texture-field", string>>
	className?: string
	static?: boolean
}
export function Pinch({ tokens, className = "", static: isStatic = false }: PinchProps) {
	const ref = useRef<HTMLDivElement>(null)
	useEffect(() => selfSize(ref.current), [])
	const geometry = useGeometry()
	useEffect(() => {
		const root = ref.current
		if (!root || isStatic || !geometry) return
		const nodes = root.querySelectorAll<SVGGraphicsElement>("svg [data-role]")
		const targets: Target[] = geometry.elements.map((e, index) => ({
			el: nodes[index]!,
			role: e.role,
			index,
			cx: e.cx,
			cy: e.cy,
		}))
		return mount(root, { spec: MOTION, targets, aspect: ASPECT, min: MIN_BOX })
	}, [isStatic, geometry])
	return (
		<div
			ref={ref}
			aria-hidden="true"
			data-texture="pinch"
			className={className}
			style={{
				position: "relative",
				width: "100%",
				height: "100%",
				overflow: "clip",
				isolation: "isolate",
				contain: "layout paint style",
				containerType: "size",
				backgroundColor: "var(--texture-field,var(--background))",
				color: "var(--texture-ink,var(--foreground))",
				...(tokens as CSSProperties),
			}}
		>
			<svg
				viewBox={`0 0 1 ${ASPECT}`}
				preserveAspectRatio="xMidYMid slice"
				style={{ position: "absolute", inset: 0, display: "block", width: "100%", height: "100%" }}
				fill="currentColor"
				fillRule="nonzero"
				focusable="false"
			>
				{(geometry?.plates ?? []).map((d, i) => (
					<path key={`p${i}`} d={d} fillOpacity={0.1} />
				))}
				{(geometry?.elements ?? []).map((e) => (
					<path key={e.id} data-role={e.role} d={e.d} />
				))}
			</svg>
		</div>
	)
}
```. Live preview: /embed/pinch.

## Agent brief

## 1. What it is

Cells, inverted: the same 26 × 18 grid of cells from the Textures file (Figma 119:367), each cell filled by its square at rest. Near the pointer the squares shrink to the smallest unit in a soft dome that trails it, and fill their cells again when it leaves. Use it as a hero or background panel, or inside a card, wherever a dense field can answer the pointer. Not for small icons: below 160 × 113 it stays static.

## 2. Behaviour timeline

- **Idle:** the resting field, every square filling its cell. Nothing runs: no loops, no listeners doing work.
- **Hover-in:** the on/off weight springs to 1 in about 80 ms; squares near the pointer shrink toward the smallest unit.
- **Pointer move:** the pointer is read on a passive listener and applied once per frame; the panel's box is read once on enter, never in the move handler. Falloff: radial, radius 0.42 of the short side, smoothstep.
- **Hover-out:** every square springs back to fill its cell (stiffness 900, damping 60); interruptible at any point.
- **Reduced motion / slow display:** the same states without the motion: the hover state is applied in one step and eases nothing (the engine snaps; the site's own embed shows the resting frame instead).
- **Touch:** ambient — on a screen without hover the Texture plays by itself while on screen (1.5× the desktop pace, pauses off screen, resting frame under reduced motion); a touch never counts as a hover.

## 3. Integration

- Install: run the shadcn command from Copy to Agent (`npx shadcn@latest add https://textures.dev/r/pinch.json`) or write the file from this Texture's markdown twin (/s/pinch.md). One self-contained React file, components/textures/pinch.tsx.
- The host provides `position: relative` and a size. **Never set width or height on the Texture; size the host.**
- Fit: `cover` by default (the panel, aspect 1 / 0.7049, covers the host and crops at the edges), `contain` available; works from 160 × 113 up; clips.
- Tokens on the root: `--texture-ink` (the squares) and `--texture-field` (the panel); both default to the shadcn `--foreground` and `--background`.

## 4. Customisation

- Change the two tokens. Do not change the spring, the falloff or the reduced-motion branch; the motion spec lives in `motion.ts` and is read by the shared runtime.

## 5. Accessibility

- Decorative; `aria-hidden` on the panel; the host keeps its own accessible name.
- Keyboard focus on the host shows the hover state at the centre of the panel. No text sits on the Texture.


## Related
Pairs with: Glow (background) · Magnet (hero) — same family, same tokens.
npx shadcn@latest add https://textures.dev/r/glow.json

## Using Textures from an agent

Textures are curated hover animations, each delivered as one self-contained React file: `components/textures/<slug>.tsx`, no Tailwind, no dependency beyond react. Three ways in, in this order:

1. **Copy to Agent** on a Texture page (https://textures.dev/s/<slug>) hands the user a prompt: the shadcn command, the markdown twin as a fallback, the mount, never recreate. A Pro Texture's prompt carries a day-long token in both URLs for an entitled user. Do what the prompt says.
2. **Connected to the Textures MCP server** (https://textures.dev/mcp): call `search_textures` with the user's words, then `get_texture` with the slug you pick, and write the one file it returns exactly as given.
3. **Neither:** `npx shadcn@latest add https://textures.dev/r/<slug>.json` installs a free Texture with no setup; or fetch `https://textures.dev/s/<slug>.md` (or the page with `Accept: text/markdown`) and write the file under Code exactly. For Pro, send a Textures API key from https://textures.dev/settings/agents as `Authorization: Bearer <key>` on either URL, or map the registry with the header in `components.json`: `"registries": { "@textures": { "url": "https://textures.dev/r/{name}.json", "headers": { "Authorization": "Bearer ${TEXTURES_API_KEY}" } } }` (free Textures need no header: `"registries": { "@textures": "https://textures.dev/r/{name}.json" }`), then `npx shadcn@latest add @textures/<slug>`.

**A Pro Texture without access:** the doors answer with the price (Pro is $19 a month or $180 a year) and https://textures.dev/checkout?texture=<slug>. Tell the user and stop. Never retry, guess or reconstruct the code.

Every published Texture is listed at https://textures.dev/llms.txt and https://textures.dev/r/registry.json. Browse them at https://textures.dev/textures.
