# Current

> 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 flat field of short dashes. The pointer is a current running through it: dashes within reach turn to lie along the direction it moves, a wake that relaxes back to flat as it passes.

- Slug: `current` · Category: lines · Free (MIT) · v0.1.0
- What moves: Dashes near the pointer turn to lie along the direction it moves and relax back to flat as it passes.
- Triggers: hover, focus, pointer-move · Touch: ambient (plays by itself on screens without hover, 1.5× pace, rests off screen) · Reduced motion: simplified
- Aliases: flow, wake, stream, liquid · Use cases: hero, background, card · Tags: dashes, grid, flow, twist, wake, liquid
- Rails: Measured 2026-10-27 for 0.1.0: 3.03 KB js, p95 14.33 ms on a 4× throttled CPU; not yet within every budget line, so no Light badge
- Estimated tokens: 3856

## Tokens

- `--texture-ink` (color, default `var(--foreground)`): Ink of the dashes
- `--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 × 112 px up; keeps 732/516 (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: 732 / 516; width: 100%; max-width: 48rem; overflow: hidden; border-radius: 4px; }
```

```tsx
<div className="host">
  <Current />
  <span className="sr-only">Current</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"><Current /></div>
  <div className="content">…</div>
</section>
```

## Install

- Command: `npx shadcn@latest add https://textures.dev/r/current.json` (one self-contained React file at components/textures/current.tsx; Pro through the Copy to Agent prompt's token URL, or `npx shadcn@latest add @textures/current` with the @textures registry and its header in components.json, see below)
- MCP: `get_texture({ ref: "current" })` (server: /mcp)
- Markdown: /s/current.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 Current hover texture (textures.dev/s/current)` (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/current.tsx`

```tsx
// Current — a Texture by Textures · https://textures.dev/s/current
// 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 } from "react"
function selfSize(root: HTMLElement | null): void {
	const host = root?.parentElement
	if (!root || !host || host.getBoundingClientRect().height > 0) return
	root.style.minHeight = "240px"
}
type Pointer = { x: number; y: number; active: number; reduced: boolean }
const AMBIENT_SPEED = 1.5
const AMBIENT_CYCLE = 4.5
type EngineOptions = { ambientSpeed?: number }
function engine(
	root: HTMLElement,
	draw: (p: Pointer, dt: number) => boolean | void,
	min: [number, number],
	options: EngineOptions = {},
): () => void {
	const svg = root.querySelector("svg")
	if (!svg) return () => {}
	const mq = () => matchMedia("(prefers-reduced-motion: reduce)").matches || matchMedia("(update: slow)").matches
	const touchMq = matchMedia("(hover: none), (pointer: coarse)")
	const forced = () => root.closest("[data-motion]")?.getAttribute("data-motion") ?? null
	const wantsAmbient = () => (forced() ? forced() === "ambient" : touchMq.matches)
	const speed = AMBIENT_SPEED * (options.ambientSpeed ?? 1)
	const w = (2 * Math.PI) / AMBIENT_CYCLE
	const phase = Math.random() * 2 * Math.PI
	let onScreen = false
	let ambient = false
	let clock = 0
	let hold = 0
	let box: { left: number; top: number; s: number; ox: number; oy: number } | null = null
	let px = 366
	let py = 258
	let cx = 0
	let cy = 0
	let active = 0
	let target = 0
	let vel = 0
	let frame = 0
	let last = 0
	let lastMoveAt = 0
	const measure = () => {
		const r = svg.getBoundingClientRect()
		if (r.width < min[0] || r.height < min[1]) {
			box = null
			return
		}
		const s = Math.max(r.width / 732, r.height / 516)
		box = { left: r.left, top: r.top, s, ox: (r.width - 732 * s) / 2, oy: (r.height - 516 * s) / 2 }
	}
	const tick = (now: number) => {
		const dt = Math.min(last ? (now - last) / 1000 : 1 / 60, 1 / 30)
		last = now
		let moving = false
		const reduced = mq()
		if (ambient && now > hold) {
			if (reduced) target = 0
			else {
				clock += dt * speed
				const t = clock
				px = 366 + 366 * (0.76 * Math.sin(w * t + phase) + 0.2 * Math.sin(1.93 * w * t + 1.2 + phase))
				py = 258 + 258 * (0.72 * Math.sin(0.75 * w * t + 0.8 + phase) + 0.2 * Math.sin(1.37 * w * t + 2.1 + phase))
				target = 1
				moving = true
			}
		}
		if (reduced) {
			active = target
			vel = 0
		} else {
			const decay = Math.exp(-30 * dt)
			const off = active - target
			const lead = vel + 30 * off
			active = target + (off + lead * dt) * decay
			vel = (vel - 30 * lead * dt) * decay
			if (Math.abs(vel) < 0.02 && Math.abs(active - target) < 0.02) {
				active = target
				vel = 0
			} else moving = true
		}
		const more = draw({ x: px, y: py, active, reduced }, dt)
		if (moving || more) frame = requestAnimationFrame(tick)
		else {
			frame = 0
			last = 0
		}
	}
	const wake = () => {
		if (!frame) frame = requestAnimationFrame(tick)
	}
	const touchIsAmbient = (e: Event) => (e as PointerEvent).pointerType === "touch" && touchMq.matches
	const onEnter = (e: PointerEvent) => {
		if (touchIsAmbient(e)) return
		measure()
	}
	const onMove = (e: PointerEvent) => {
		if (touchIsAmbient(e)) return
		const t = performance.now()
		if (!box || t - lastMoveAt > 250) measure()
		lastMoveAt = t
		if (!box) return
		if (!e.isTrusted) hold = performance.now() + 1000
		cx = e.clientX
		cy = e.clientY
		px = (cx - box.left - box.ox) / box.s
		py = (cy - box.top - box.oy) / box.s
		target = 1
		wake()
	}
	const onLeave = (e?: Event) => {
		if (e && touchIsAmbient(e)) return
		if (e && !e.isTrusted) hold = 0
		if (ambient && performance.now() > hold) return
		target = 0
		wake()
	}
	const onFocus = () => {
		measure()
		if (!box) return
		px = 366
		py = 258
		target = 1
		wake()
	}
	const onResize = () => {
		if (box || ambient) measure()
		if (ambient && !box) ambient = false
		settle()
	}
	const onScroll = () => {
		if (!box || target !== 1 || (ambient && performance.now() > hold)) return
		measure()
		if (!box) return
		px = (cx - box.left - box.ox) / box.s
		py = (cy - box.top - box.oy) / box.s
		wake()
	}
	const settle = () => {
		const next = onScreen && !document.hidden && wantsAmbient()
		if (next === ambient) return
		ambient = next
		if (ambient) {
			measure()
			if (!box) {
				ambient = false
				return
			}
			wake()
		} else {
			target = 0
			wake()
		}
	}
	const io =
		typeof IntersectionObserver === "function"
			? new IntersectionObserver(
					([entry]) => {
						onScreen = !!entry?.isIntersecting
						settle()
					},
					{ threshold: 0.2 },
				)
			: null
	io?.observe(svg)
	if (!io) {
		onScreen = true
		settle()
	}
	const onVisibility = () => settle()
	const onCapability = () => settle()
	const reducedMq = matchMedia("(prefers-reduced-motion: reduce)")
	const onReduced = () => wake()
	document.addEventListener("visibilitychange", onVisibility)
	touchMq.addEventListener?.("change", onCapability)
	reducedMq.addEventListener?.("change", onReduced)
	const host: Element = root.closest("a, button, [tabindex]") ?? root.parentElement ?? root
	root.addEventListener("pointerenter", onEnter)
	root.addEventListener("pointermove", onMove, { passive: true })
	root.addEventListener("pointerleave", onLeave)
	root.addEventListener("pointercancel", onLeave)
	host.addEventListener("focusin", onFocus)
	host.addEventListener("focusout", onLeave)
	window.addEventListener("resize", onResize, { passive: true })
	window.addEventListener("scroll", onScroll, { passive: true, capture: true })
	return () => {
		root.removeEventListener("pointerenter", onEnter)
		root.removeEventListener("pointermove", onMove)
		root.removeEventListener("pointerleave", onLeave)
		root.removeEventListener("pointercancel", onLeave)
		host.removeEventListener("focusin", onFocus)
		host.removeEventListener("focusout", onLeave)
		window.removeEventListener("resize", onResize)
		window.removeEventListener("scroll", onScroll, { capture: true })
		document.removeEventListener("visibilitychange", onVisibility)
		touchMq.removeEventListener?.("change", onCapability)
		reducedMq.removeEventListener?.("change", onReduced)
		io?.disconnect()
		if (frame) cancelAnimationFrame(frame)
	}
}
const W = 732
const H = 516
const DASH_H = 5
const XS: number[] = [
	16, 50, 83, 117, 150, 184, 217, 251, 284, 318, 351, 385, 418, 452, 485, 519, 552, 586, 619, 653, 686,
]
const WIDTHS: number[] = XS.map((_, i) => (i % 2 === 0 ? 30 : 29))
const ROWS = 24
const Y0 = 16
const DY = (495 - 16) / (ROWS - 1)
const COLS = XS.length
const DASHES: [number, number, number][] = []
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) DASHES.push([XS[c]!, Y0 + r * DY, WIDTHS[c]!])
export type CurrentProps = {
	tokens?: Partial<Record<"--texture-ink" | "--texture-field", string>>
	className?: string
	static?: boolean
}
const STEP = 45
const SPAN = 2
const RADIUS = 2.5
const TAU_SNAP = 0.03
const TAU_RING = 0.04
const TAU_CORNER = 0.025
const TAU_HOME = 0.045
const TAU_HOME_RING = 0.03
const DIR_EASE = 0.5
const STILL = 0.4
const HYSTERESIS = 8
const DEAD = 0.05
const arc = (deg: number) => (((deg % 180) + 270) % 180) - 90
export function Current({ tokens, className = "", static: isStatic = false }: CurrentProps) {
	const ref = useRef<HTMLDivElement>(null)
	useEffect(() => selfSize(ref.current), [])
	const rects = useRef<(SVGRectElement | null)[]>([])
	useEffect(() => {
		const root = ref.current
		if (!root || isStatic) return
		const n = DASHES.length
		const angle = new Float32Array(n)
		const target = new Float32Array(n)
		const tau = new Float32Array(n).fill(TAU_HOME)
		const home = new Float32Array(n).fill(TAU_HOME)
		const written = new Float32Array(n)
		const centres = XS.map((x, i) => x + DASHES[i]![2] / 2)
		let lx = NaN
		let ly = NaN
		let dx = 1
		let dy = 0
		let step = 0
		let lastActive = 0
		return engine(
			root,
			({ x, y, active, reduced }, dt) => {
				if (!Number.isNaN(lx)) {
					const sx = x - lx
					const sy = y - ly
					const len = Math.hypot(sx, sy)
					if (len > STILL) {
						const k = reduced ? 1 : 1 - (1 - DIR_EASE) ** (dt * 60)
						dx += (sx / len - dx) * k
						dy += (sy / len - dy) * k
						const m = Math.hypot(dx, dy) || 1
						dx /= m
						dy /= m
						const deg = ((((Math.atan2(dy, dx) * 180) / Math.PI) % 180) + 180) % 180
						const away = Math.abs(arc(deg - step))
						if (away > STEP / 2 + HYSTERESIS) step = (Math.round(deg / STEP) * STEP) % 180
					}
				}
				lx = x
				ly = y
				target.fill(0)
				const on = active > 0.5 || active > lastActive
				lastActive = active
				if (on) {
					let c0 = 0
					for (let c = 1; c < COLS; c++) if (Math.abs(centres[c]! - x) < Math.abs(centres[c0]! - x)) c0 = c
					const r0 = Math.min(ROWS - 1, Math.max(0, Math.round((y - Y0 - DASH_H / 2) / DY)))
					const reach = Math.floor(RADIUS)
					for (let j = -reach; j <= reach; j++)
						for (let k = -reach; k <= reach; k++) {
							if (j * j + k * k > RADIUS * RADIUS) continue
							const c = c0 + k * SPAN
							const r = r0 + j * SPAN
							if (c < 0 || c >= COLS || r < 0 || r >= ROWS) continue
							const i = r * COLS + c
							const ring = Math.max(Math.abs(j), Math.abs(k))
							const corner = j !== 0 && k !== 0
							target[i] = arc(corner ? step + STEP : step)
							tau[i] = TAU_SNAP + ring * TAU_RING + (corner ? TAU_CORNER : 0)
							home[i] = TAU_HOME + ring * TAU_HOME_RING
						}
				}
				let moving = false
				for (let i = 0; i < n; i++) {
					const a = angle[i]!
					const t = target[i]!
					if (a === t) {
						if (t === 0) tau[i] = home[i]!
						continue
					}
					let next: number
					if (reduced) next = t
					else {
						const way = arc(t - a)
						next = a + way * (1 - Math.exp(-dt / tau[i]!))
						if (Math.abs(arc(t - next)) < DEAD) next = t
					}
					angle[i] = next
					if (next !== t) moving = true
					else if (t === 0) tau[i] = home[i]!
					if (Math.abs(next - written[i]!) > 0.05 || (next === 0 && written[i] !== 0)) {
						written[i] = next
						const el = rects.current[i]
						if (el) el.style.transform = next === 0 ? "" : `rotate(${next.toFixed(2)}deg)`
					}
				}
				return moving
			},
			[160, 112],
		)
	}, [isStatic])
	const style = {
		"--texture-ink": "var(--foreground)",
		"--texture-field": "var(--background)",
		...tokens,
		color: "var(--texture-ink)",
		background: "var(--texture-field)",
	} as CSSProperties
	return (
		<div
			ref={ref}
			style={{
				position: "relative",
				isolation: "isolate",
				height: "100%",
				width: "100%",
				overflow: "clip",
				contain: "layout paint style",
				containerType: "size",
				...style,
			}}
			data-texture="current"
			className={className}
		>
			<svg
				viewBox={`0 0 ${W} ${H}`}
				preserveAspectRatio="xMidYMid slice"
				aria-hidden="true"
				focusable="false"
				fill="currentColor"
				style={{ position: "absolute", inset: 0, display: "block", height: "100%", width: "100%" }}
			>
				{DASHES.map(([x, y, w], i) => (
					<rect
						key={i}
						ref={(el) => {
							rects.current[i] = el
						}}
						x={x}
						y={y}
						width={w}
						height={DASH_H}
						style={{ transformBox: "fill-box", transformOrigin: "center" }}
					/>
				))}
			</svg>
		</div>
	)
}
```. Live preview: /embed/current.

## Agent brief

## 1. What it is

A field of short dashes resting flat, taken 1:1 from the Textures file (Figma 119:111): twenty-one columns and twenty-four rows on the 732 × 516 panel. The pointer is a current running through it: the dashes within reach turn to lie along the direction it is moving, fully under the cursor and fading out over about two cells, so the field twists into a wake behind the pointer and relaxes back to flat as it passes. Use it as a hero or background panel, or inside a card, wherever a quiet field can answer the pointer. Not for small icons: below 160 × 112 it stays static.

## 2. Behaviour timeline

- **Idle:** the flat field. Nothing runs: no loops, no listeners doing work.
- **Hover-in:** the on/off weight springs to 1 in about 80 ms; the dashes under the pointer turn to its direction of travel as soon as it moves.
- **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. The direction is the pointer's last step, smoothed as a vector. A dash under the cursor answers in the same frame (30 ms time constant); at the edge of its reach a dash follows about 200 ms later, so the turn runs through the field like a liquid.
- **Hover-out:** every dash eases back to flat (240 ms time constant); 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/current.json`) or write the file from this Texture's markdown twin (/s/current.md). One self-contained React file, components/textures/current.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 732 × 516 panel covers the host and crops at the edges), `contain` available; works from 160 × 112 up; clips.
- Tokens on the root: `--texture-ink` (the dashes) and `--texture-field` (the panel); both default to the shadcn `--foreground` and `--background`.

## 4. Customisation

- Change the two tokens. Do not change the reach, the time constants or the reduced-motion branch.

## 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: Settle (hero) · Beam (background) — same family, same tokens.
npx shadcn@latest add https://textures.dev/r/settle.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.
