{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "imprint",
  "type": "registry:ui",
  "title": "Imprint",
  "description": "Letterpress image trail: pointer travel stamps a pile of proofs that land with a settle, a seeded tilt, a registration double and an ink-spread shadow, then fade and desaturate like aging paper. Clicks stack larger proofs, a seeded ghost path keeps printing when idle, and an imperative stamp(x, y) handle; reduced motion rests on a static corner pile.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://crucibleui.com/r/use-reduced-motion.json",
    "https://crucibleui.com/r/use-visibility-pause.json"
  ],
  "files": [
    {
      "path": "registry/default/cursors/imprint/imprint.tsx",
      "type": "registry:ui",
      "target": "components/crucible/imprint.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibilityPause } from \"@/hooks/use-visibility-pause\";\n\n/** One thing Imprint can stamp: a picture, a gradient tile, or a set word. */\nexport type ImprintItem =\n  | { /** Image URL. */ src: string; /** Optional caption in the paper margin. */ label?: string }\n  | { /** Any CSS background (gradient or color). */ gradient: string; label?: string }\n  | { /** Text printed large on the proof. */ text: string; label?: string };\n\nexport interface ImprintHandle {\n  /**\n   * Stamp at container-local coordinates (px from the top-left of the scope).\n   * Omit both to stamp at the center. Pass `proof: true` for the larger click stamp.\n   */\n  stamp: (x?: number, y?: number, options?: { proof?: boolean }) => void;\n}\n\nexport interface ImprintProps {\n  /** Content inside the scope. Stays fully interactive; the trail prints behind it. */\n  children?: React.ReactNode;\n  className?: string;\n  /**\n   * What gets stamped, cycled in order. Defaults to a seeded set of soft\n   * slate/graphite gradient tiles with short catalog labels.\n   */\n  items?: ImprintItem[];\n  /** Pointer travel (px) between stamps. @default 90 */\n  spacing?: number;\n  /** Stamp width in px (height is 5:4 of it). Click proofs are 1.45x. @default 140 */\n  size?: number;\n  /** Most stamps on the pile at once; the oldest lift off beyond it. @default 12 */\n  max?: number;\n  /** How long (ms) a stamp rests before it fades and desaturates away. @default 1600 */\n  life?: number;\n  /** Max seeded tilt either way, in degrees. @default 6 */\n  rotate?: number;\n  /** Ghost autoplay along a seeded Lissajous path after 1.5s without a pointer. @default true */\n  idle?: boolean;\n  /** Seed for the ghost path, tilts, registration offsets and default tiles. @default 7 */\n  seed?: number;\n  /** Stop stamping (pointer and ghost). Existing stamps finish aging out. @default false */\n  paused?: boolean;\n  /** React 19 ref prop — imperative `stamp(x, y)`. */\n  ref?: React.Ref<ImprintHandle>;\n}\n\ninterface Stamp {\n  id: number;\n  x: number;\n  y: number;\n  rot: number;\n  dx: number;\n  dy: number;\n  item: number;\n  proof: boolean;\n  retiring: boolean;\n}\n\nconst IDLE_MS = 1500;\nconst GHOST_SPEED = 300; // px/s along the ghost path\nconst MAX_PER_MOVE = 4; // stamps interpolated from a single fast pointer event\nconst LAND_S = 0.16;\nconst AGE_S = 0.65;\nconst RETIRE_S = 0.2;\n\n/** Deterministic 0–1 hash of (seed, n) — stable across server and client. */\nfunction hash01(seed: number, n: number): number {\n  let t = (seed * 0x9e3779b1 + n * 0x85ebca6b) | 0;\n  t = Math.imul(t ^ (t >>> 15), t | 1);\n  t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n}\n\nfunction defaultItems(seed: number): ImprintItem[] {\n  const out: ImprintItem[] = [];\n  for (let i = 0; i < 8; i++) {\n    const r = (k: number) => hash01(seed + 101, i * 7 + k);\n    // Neutral-cool family: slate, graphite, fog, one muted ink blue.\n    const hue = 205 + r(0) * 30;\n    const sat = i % 4 === 2 ? 38 : 6 + r(1) * 14;\n    const l1 = 18 + r(2) * 22;\n    const l2 = l1 + 22 + r(3) * 30;\n    const angle = Math.round(r(4) * 360);\n    const spot = `radial-gradient(circle at ${Math.round(20 + r(5) * 60)}% ${Math.round(15 + r(6) * 50)}%, hsl(${hue.toFixed(0)} ${sat.toFixed(0)}% ${Math.min(92, l2 + 18).toFixed(0)}% / 0.75), transparent 55%)`;\n    out.push({\n      gradient: `${spot}, linear-gradient(${angle}deg, hsl(${hue.toFixed(0)} ${sat.toFixed(0)}% ${l1.toFixed(0)}%), hsl(${(hue + 12).toFixed(0)} ${sat.toFixed(0)}% ${l2.toFixed(0)}%))`,\n      label: `No. ${String(i + 1).padStart(2, \"0\")}`,\n    });\n  }\n  return out;\n}\n\nfunction ItemFace({ item }: { item: ImprintItem }) {\n  if (\"src\" in item) {\n    return (\n      // eslint-disable-next-line @next/next/no-img-element -- shipped registry source stays framework-agnostic\n      <img src={item.src} alt=\"\" draggable={false} className=\"h-full w-full object-cover\" />\n    );\n  }\n  if (\"gradient\" in item) {\n    return <div className=\"h-full w-full\" style={{ background: item.gradient }} />;\n  }\n  return (\n    <div className=\"flex h-full w-full items-center justify-center bg-neutral-900 p-2 text-center font-serif text-[1.35em] leading-none font-semibold tracking-tight text-neutral-100\">\n      {item.text}\n    </div>\n  );\n}\n\n/** Paper proof: neutral sheet with an inked plate and a caption in the margin. */\nfunction Proof({ item, w, h, proof }: { item: ImprintItem; w: number; h: number; proof: boolean }) {\n  const pad = Math.max(4, Math.round(w * 0.045));\n  const label = item.label ?? (proof ? \"Proof\" : undefined);\n  return (\n    <div\n      className=\"flex h-full w-full flex-col bg-neutral-100\"\n      style={{ padding: pad, paddingBottom: label ? pad * 3.2 : pad, fontSize: Math.max(10, w * 0.12) }}\n    >\n      <div className=\"relative min-h-0 flex-1 overflow-hidden\">\n        <ItemFace item={item} />\n        {/* Ink texture: a soft inner edge so the plate reads pressed, not pasted. */}\n        <div className=\"pointer-events-none absolute inset-0 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12),inset_0_0_12px_rgba(0,0,0,0.18)]\" />\n      </div>\n      {label ? (\n        <div\n          className=\"flex items-end justify-between font-mono tracking-[0.12em] text-neutral-500 uppercase\"\n          style={{ height: pad * 2.2, fontSize: Math.max(7, Math.min(11, w * 0.06)) }}\n        >\n          <span>{proof ? \"Proof\" : label}</span>\n          <span>{Math.round(w)}×{Math.round(h)}</span>\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nconst StampView = React.memo(function StampView({\n  stamp,\n  item,\n  size,\n  life,\n  onDone,\n}: {\n  stamp: Stamp;\n  item: ImprintItem;\n  size: number;\n  life: number;\n  onDone: (id: number) => void;\n}) {\n  const w = stamp.proof ? size * 1.45 : size;\n  const h = w * 1.25;\n  return (\n    <motion.div\n      className=\"absolute\"\n      style={{ left: stamp.x - w / 2, top: stamp.y - h / 2, width: w, height: h, rotate: stamp.rot }}\n      initial={{ scale: stamp.proof ? 1.12 : 1.06, y: stamp.proof ? 7 : 4, opacity: 0 }}\n      animate={{ scale: 1, y: 0, opacity: 1 }}\n      transition={{\n        duration: stamp.proof ? LAND_S * 1.4 : LAND_S,\n        ease: [0.2, 0.8, 0.3, 1],\n        opacity: { duration: 0.06 },\n      }}\n    >\n      <motion.div\n        className=\"relative h-full w-full\"\n        initial={{ opacity: 1, filter: \"grayscale(0) brightness(1) contrast(1)\" }}\n        animate={\n          stamp.retiring\n            ? // Distinct target so motion restarts with the fast retire transition.\n              { opacity: 0, filter: \"grayscale(1) brightness(0.8) contrast(0.9)\" }\n            : { opacity: 0, filter: \"grayscale(1) brightness(0.85) contrast(0.9)\" }\n        }\n        transition={\n          stamp.retiring\n            ? { duration: RETIRE_S, ease: \"easeOut\" }\n            : {\n                delay: life / 1000,\n                duration: AGE_S,\n                ease: \"easeIn\",\n                // Desaturate leads the fade so it reads as paper aging, not vanishing.\n                filter: { delay: life / 1000 - AGE_S * 0.4, duration: AGE_S * 1.2, ease: \"easeOut\" },\n              }\n        }\n        onAnimationComplete={() => onDone(stamp.id)}\n      >\n        {/* Ink spread: blooms on impact, then tightens to a resting shadow. */}\n        <motion.div\n          className=\"absolute inset-0\"\n          initial={{ boxShadow: \"0px 0px 0px 0px rgba(0,0,0,0)\" }}\n          animate={{\n            boxShadow: [\n              \"0px 0px 0px 0px rgba(0,0,0,0)\",\n              stamp.proof ? \"0px 22px 52px 10px rgba(0,0,0,0.6)\" : \"0px 16px 38px 6px rgba(0,0,0,0.55)\",\n              stamp.proof ? \"0px 10px 22px 0px rgba(0,0,0,0.5)\" : \"0px 6px 14px 0px rgba(0,0,0,0.45)\",\n            ],\n          }}\n          transition={{ duration: 0.45, times: [0, 0.3, 1], ease: \"easeOut\" }}\n        />\n        <div className=\"absolute inset-0 overflow-hidden\">\n          <Proof item={item} w={w} h={h} proof={stamp.proof} />\n        </div>\n        {/* Registration copy: a faint misaligned second pass that snaps into place. */}\n        <motion.div\n          className=\"absolute inset-0 overflow-hidden\"\n          initial={{ x: stamp.dx, y: stamp.dy, opacity: 0.18 }}\n          animate={{ x: 0, y: 0, opacity: [0.18, 0.18, 0] }}\n          transition={{ duration: 0.24, ease: [0.5, 0, 0.2, 1], opacity: { duration: 0.3, times: [0, 0.75, 1] } }}\n        >\n          <Proof item={item} w={w} h={h} proof={stamp.proof} />\n        </motion.div>\n      </motion.div>\n    </motion.div>\n  );\n});\n\n/**\n * Imprint — a letterpress image trail. Moving the pointer (mouse, pen or\n * touch) through the scoped container stamps a pile of proofs every `spacing`\n * px of travel. Each lands with a quick settle, a seeded tilt, a faint\n * registration double that snaps into place and an ink-spread shadow that\n * blooms then tightens; after `life` ms it desaturates and fades like aging\n * paper. Clicks stack larger \"proof\" stamps. With no pointer for 1.5s a seeded\n * Lissajous ghost keeps printing (paused offscreen and on hidden tabs). The\n * trail layer is decorative and `pointer-events-none`; children stay\n * interactive. Under reduced motion there is no trail — a static loose pile\n * of three proofs rests in the corner.\n */\nexport function Imprint({\n  children,\n  className,\n  items,\n  spacing = 90,\n  size = 140,\n  max = 12,\n  life = 1600,\n  rotate = 6,\n  idle = true,\n  seed = 7,\n  paused = false,\n  ref,\n}: ImprintProps) {\n  const reducedMotion = useReducedMotion();\n  const fallbackItems = React.useMemo(() => defaultItems(seed), [seed]);\n  const pool = items && items.length > 0 ? items : fallbackItems;\n\n  const [stamps, setStamps] = React.useState<Stamp[]>([]);\n\n  // Hot-path state lives in refs; React only hears about it once per frame.\n  const pendingRef = React.useRef<Stamp[]>([]);\n  const flushRafRef = React.useRef(0);\n  const counterRef = React.useRef(0);\n  const anchorRef = React.useRef<{ x: number; y: number } | null>(null);\n  const lastActivityRef = React.useRef(0);\n  const sizeRef = React.useRef({ w: 0, h: 0 });\n  const ghostRef = React.useRef<{ t: number; last: number; travel: number; px: number; py: number; on: boolean }>({\n    t: 0,\n    last: 0,\n    travel: 0,\n    px: 0,\n    py: 0,\n    on: false,\n  });\n\n  const optsRef = React.useRef({ spacing, max, rotate, seed, pool, paused, reducedMotion });\n  optsRef.current = { spacing, max, rotate, seed, pool, paused, reducedMotion };\n\n  const flush = React.useCallback(() => {\n    flushRafRef.current = 0;\n    const fresh = pendingRef.current;\n    if (fresh.length === 0) return;\n    pendingRef.current = [];\n    const cap = Math.max(1, optsRef.current.max);\n    setStamps((prev) => {\n      let next = prev.concat(fresh);\n      // Beyond the cap the oldest live stamps lift off quickly instead of popping.\n      let live = 0;\n      for (let i = next.length - 1; i >= 0; i--) {\n        if (next[i].retiring) continue;\n        live++;\n        if (live > cap) next[i] = { ...next[i], retiring: true };\n      }\n      // Hard ceiling on DOM nodes even under extreme input.\n      if (next.length > cap * 2) next = next.slice(next.length - cap * 2);\n      return next;\n    });\n  }, []);\n\n  const enqueue = React.useCallback(\n    (x: number, y: number, proof = false) => {\n      const o = optsRef.current;\n      if (o.reducedMotion) return;\n      const n = counterRef.current++;\n      const r = (k: number) => hash01(o.seed, n * 5 + k);\n      const angle = r(2) * Math.PI * 2;\n      const off = 1 + r(3); // 1–2px registration error\n      pendingRef.current.push({\n        id: n,\n        x,\n        y,\n        rot: (r(0) * 2 - 1) * o.rotate * (proof ? 0.6 : 1),\n        dx: Math.cos(angle) * off,\n        dy: Math.sin(angle) * off,\n        item: n % o.pool.length,\n        proof,\n        retiring: false,\n      });\n      if (!flushRafRef.current) flushRafRef.current = requestAnimationFrame(flush);\n    },\n    [flush]\n  );\n\n  const onDone = React.useCallback((id: number) => {\n    setStamps((prev) => prev.filter((s) => s.id !== id));\n  }, []);\n\n  /** Advance an anchor toward (x, y), stamping every `spacing` px of travel. */\n  const travel = React.useCallback(\n    (x: number, y: number) => {\n      const step = Math.max(8, optsRef.current.spacing);\n      const a = anchorRef.current;\n      if (!a) {\n        anchorRef.current = { x, y };\n        enqueue(x, y);\n        return;\n      }\n      let dist = Math.hypot(x - a.x, y - a.y);\n      let made = 0;\n      while (dist >= step && made < MAX_PER_MOVE) {\n        const f = step / dist;\n        a.x += (x - a.x) * f;\n        a.y += (y - a.y) * f;\n        enqueue(a.x, a.y);\n        made++;\n        dist = Math.hypot(x - a.x, y - a.y);\n      }\n      // A flick longer than the per-event budget re-anchors at the pointer.\n      if (made === MAX_PER_MOVE && dist >= step) {\n        a.x = x;\n        a.y = y;\n      }\n    },\n    [enqueue]\n  );\n\n  // Ghost driver: rAF via useVisibilityPause, so it sleeps offscreen / hidden.\n  const tick = React.useCallback(\n    (elapsed: number) => {\n      const g = ghostRef.current;\n      const dt = Math.min(Math.max(elapsed - g.last, 0), 0.05);\n      g.last = elapsed;\n      const { w, h } = sizeRef.current;\n      if (w < 2 || h < 2) return;\n      const now = performance.now();\n      if (now - lastActivityRef.current < IDLE_MS) {\n        g.on = false;\n        return;\n      }\n      const s = optsRef.current.seed;\n      const fa = 2 + Math.floor(hash01(s, 900) * 2); // 2 or 3\n      const fb = fa + 1 + Math.floor(hash01(s, 901) * 2);\n      const phase = hash01(s, 902) * Math.PI * 2;\n      const ax = w * 0.36;\n      const ay = h * 0.32;\n      // Parametric speed roughly normalised to GHOST_SPEED px/s.\n      g.t += (dt * GHOST_SPEED) / Math.max(120, (ax * fa + ay * fb) * 0.75);\n      const x = w / 2 + ax * Math.sin(fa * g.t + phase);\n      const y = h / 2 + ay * Math.sin(fb * g.t);\n      if (!g.on) {\n        g.on = true;\n        g.px = x;\n        g.py = y;\n        g.travel = optsRef.current.spacing * 0.6;\n        return;\n      }\n      g.travel += Math.hypot(x - g.px, y - g.py);\n      g.px = x;\n      g.py = y;\n      if (g.travel >= Math.max(8, optsRef.current.spacing)) {\n        g.travel = 0;\n        enqueue(x, y);\n      }\n    },\n    [enqueue]\n  );\n\n  const scopeRef = useVisibilityPause<HTMLDivElement>(tick, { paused: !idle || paused || reducedMotion });\n\n  React.useEffect(() => {\n    const el = scopeRef.current;\n    if (!el) return;\n    lastActivityRef.current = performance.now();\n\n    const ro = new ResizeObserver(() => {\n      sizeRef.current = { w: el.clientWidth, h: el.clientHeight };\n    });\n    ro.observe(el);\n    sizeRef.current = { w: el.clientWidth, h: el.clientHeight };\n\n    const local = (e: PointerEvent) => {\n      const rect = el.getBoundingClientRect();\n      return { x: e.clientX - rect.left, y: e.clientY - rect.top };\n    };\n    const onMove = (e: PointerEvent) => {\n      lastActivityRef.current = performance.now();\n      ghostRef.current.on = false;\n      if (optsRef.current.paused) return;\n      const p = local(e);\n      travel(p.x, p.y);\n    };\n    const onDown = (e: PointerEvent) => {\n      lastActivityRef.current = performance.now();\n      if (optsRef.current.paused || e.button > 0) return;\n      const p = local(e);\n      enqueue(p.x, p.y, true);\n      anchorRef.current = { x: p.x, y: p.y };\n    };\n    const onLeave = () => {\n      anchorRef.current = null;\n    };\n\n    el.addEventListener(\"pointermove\", onMove);\n    el.addEventListener(\"pointerdown\", onDown);\n    el.addEventListener(\"pointerleave\", onLeave);\n    el.addEventListener(\"pointercancel\", onLeave);\n    return () => {\n      ro.disconnect();\n      el.removeEventListener(\"pointermove\", onMove);\n      el.removeEventListener(\"pointerdown\", onDown);\n      el.removeEventListener(\"pointerleave\", onLeave);\n      el.removeEventListener(\"pointercancel\", onLeave);\n      if (flushRafRef.current) cancelAnimationFrame(flushRafRef.current);\n      flushRafRef.current = 0;\n      pendingRef.current = [];\n      anchorRef.current = null;\n    };\n  }, [scopeRef, travel, enqueue]);\n\n  // Reduced motion (or a live toggle into it): clear the trail.\n  React.useEffect(() => {\n    if (reducedMotion) {\n      pendingRef.current = [];\n      setStamps([]);\n    }\n  }, [reducedMotion]);\n\n  React.useImperativeHandle(\n    ref,\n    () => ({\n      stamp(x, y, options) {\n        const { w, h } = sizeRef.current;\n        enqueue(x ?? w / 2, y ?? h / 2, options?.proof ?? false);\n      },\n    }),\n    [enqueue]\n  );\n\n  return (\n    <div ref={scopeRef} data-crucible=\"imprint\" className={cn(\"relative isolate overflow-hidden\", className)}>\n      <div aria-hidden className=\"pointer-events-none absolute inset-0 -z-10 overflow-hidden select-none\">\n        {reducedMotion ? (\n          <StaticPile pool={pool} size={size} seed={seed} rotate={rotate} />\n        ) : (\n          stamps.map((s) => (\n            <StampView\n              key={s.id}\n              stamp={s}\n              item={pool[s.item % pool.length]}\n              size={size}\n              life={life}\n              onDone={onDone}\n            />\n          ))\n        )}\n      </div>\n      {children}\n    </div>\n  );\n}\n\n/** Reduced-motion resting frame: three proofs loosely piled in the bottom-right corner. */\nfunction StaticPile({ pool, size, seed, rotate }: { pool: ImprintItem[]; size: number; seed: number; rotate: number }) {\n  const w = Math.min(size, 120);\n  const h = w * 1.25;\n  return (\n    <div className=\"absolute right-6 bottom-6\" style={{ width: w * 1.5, height: h * 1.25 }}>\n      {[0, 1, 2].map((i) => {\n        const rot = (hash01(seed, 500 + i) * 2 - 1) * Math.max(rotate, 4) * 1.4;\n        return (\n          <div\n            key={i}\n            className=\"absolute shadow-[0_6px_14px_rgba(0,0,0,0.45)]\"\n            style={{\n              width: w,\n              height: h,\n              right: i * w * 0.18,\n              bottom: i * h * 0.08,\n              transform: `rotate(${rot.toFixed(2)}deg)`,\n              filter: i === 2 ? undefined : `grayscale(${0.5 - i * 0.25})`,\n              opacity: 0.75 + i * 0.12,\n            }}\n          >\n            <Proof item={pool[i % pool.length]} w={w} h={h} proof={false} />\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n"
    }
  ]
}