{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "imprint",
  "type": "registry:ui",
  "title": "Imprint",
  "description": "A background wall of boxes that takes an imprint of the pointer. The boxes under it sink back along the z axis to one flat floor, the boxes around them stand in front with their side walls showing, and the dent fills back in slowly behind the pointer. Press to push deeper and roll a ring out across the wall.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "background",
    "interaction",
    "canvas",
    "3d"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/imprint",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/imprint.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - One canvas and one rAF loop draw the whole wall. The loop stops once every box settles or the\n *   wall leaves the screen, and pointer events only `wake()` it, so nothing re-renders in React.\n * - Each box picks its spring per frame: the stiff, near-critical one while its target is deeper,\n *   the soft one that bobs past the surface otherwise. That split is what leaves the trail.\n * - Boxes paint deepest first, then the surface, then any box bobbing in front of it, and a side\n *   wall is drawn only where a neighbor sits deeper and the wall faces the viewer.\n * - The dent aims below the pointer by the tilt, so the floor, seen from above, lands under it.\n * - Reduced motion sets each box straight to its target and the press straight to held, with no\n *   springs, and no ring rolls out.\n */\nconst TIMING = {\n  /** The spring a box sinks on: stiffness (the `sinkStiffness` prop) and damping ratio. */\n  sinkStiffness: 340,\n  sinkDamping: 0.9,\n  /**\n   * The spring a box rises back on: stiffness (the `riseStiffness` prop) and damping ratio (the\n   * `riseDamping` prop), under 1 so a box bobs past the surface.\n   */\n  riseStiffness: 64,\n  riseDamping: 0.62,\n  /** The press pushing the floor deeper and letting go: time constants, in seconds. */\n  pressIn: 0.06,\n  pressOut: 0.2,\n  /** How fast the ring from a press rolls out, in px/s (the `rippleSpeed` prop). */\n  rippleSpeed: 820,\n  /** The time constant the ring's height decays on, in seconds. */\n  ringDecay: 0.5,\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * IMPRINT STORYBOARD\n *\n *  rest     a wall of square boxes, fronts flush, like a pin-art\n *           toy seen face on. Only the seams show\n *  hover    boxes under the pointer sink back along the z axis.\n *           Inside `reach` they settle on one flat floor; the rim\n *           steps down to it. The boxes still at the surface\n *           stand in front of the sunk ones, so their side walls\n *           show and they overlap the floor's edge, seen from a\n *           little above (`tilt`) and in perspective\n *  move     sinking is quick, rising is slow: the dent follows\n *           the pointer and leaves a soft trail that fills back\n *           in, each box bobbing just past the surface\n *  press    holding the pointer down pushes the floor deeper,\n *           and a ring rolls out across the wall from the press\n *  leave    every box springs back to the surface\n *  reduced  the dent follows the pointer without springs; no\n *           ring\n * ───────────────────────────────────────────────────────── */\n\nconst SURFACE = {\n  shadow: 0.7, // a floor box under a higher neighbor above or to its left shades as if this much deeper, share of the step\n  pressBoost: 0.6, // extra depth while the pointer is held down, share of `depth`\n  ring: { width: 1.1, amp: 0.42 }, // cells, share of `depth`; it rolls out at `rippleSpeed` and fades on `TIMING.ringDecay`\n  levels: 18, // shading steps between the surface and the deepest floor\n  rest: 0.02, // px, a box this close to its target and nearly still is settled\n  maxDpr: 2,\n} as const;\n\n// How each face is shaded: `base` mixes toward white (+) or black (−), `deep` is added\n// at the deepest floor, `accent` is the accent's share there. Light comes from the top left.\n// `dark` replaces `base` and `deep` when the boxes are a dark color, where black barely shows.\ntype Shading = { base: number; deep: number; accent: number; dark?: { base: number; deep: number } };\nconst FACES = {\n  front: { base: 0, deep: -0.3, accent: 0.85, dark: { base: 0, deep: -0.5 } },\n  seam: { base: -0.09, deep: -0.36, accent: 0.7, dark: { base: 0.07, deep: -0.5 } }, // the line around each front\n  up: { base: 0.2, deep: -0.12, accent: 0.4, dark: { base: 0.16, deep: -0.2 } }, // the wall facing up, at the bottom of a dent\n  left: { base: 0.08, deep: -0.12, accent: 0.3, dark: { base: 0.07, deep: -0.2 } },\n  right: { base: -0.16, deep: -0.12, accent: 0.25, dark: { base: -0.25, deep: -0.2 } },\n  down: { base: -0.3, deep: -0.12, accent: 0.2, dark: { base: -0.4, deep: -0.2 } },\n} satisfies Record<string, Shading>;\n\ntype Face = keyof typeof FACES;\nconst FACE_NAMES = Object.keys(FACES) as Face[];\n\ntype Options = {\n  cell: number;\n  gap: number;\n  depth: number;\n  reach: number;\n  falloff: number;\n  tilt: number;\n  perspective: number;\n  ripple: boolean;\n  sinkStiffness: number;\n  riseStiffness: number;\n  riseDamping: number; // damping ratio, under 1 so a box bobs past the surface\n  rippleSpeed: number; // px/s\n  reduced: boolean;\n};\n\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\n\n// Whether a CSS color is dark, by painting it on a 1×1 canvas (computed colors may be oklch or color()).\nfunction isDark(color: string) {\n  const ctx = document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n  if (!ctx) return false;\n  ctx.fillStyle = color;\n  ctx.fillRect(0, 0, 1, 1);\n  const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;\n  return 0.2126 * r + 0.7152 * g + 0.0722 * b < 128;\n}\n\nfunction createImprint() {\n  let root: HTMLElement | null = null;\n  let canvas: HTMLCanvasElement | null = null;\n  let ctx: CanvasRenderingContext2D | null = null;\n  let probes: { face: HTMLElement; accent: HTMLElement } | null = null;\n  let tinted = false; // an accent class is set\n  const palette = {} as Record<Face, string[]>;\n\n  let options: Options = {\n    cell: 48,\n    gap: 2,\n    depth: 48,\n    reach: 140,\n    falloff: 0.45,\n    tilt: 0.35,\n    perspective: 900,\n    ripple: true,\n    sinkStiffness: TIMING.sinkStiffness,\n    riseStiffness: TIMING.riseStiffness,\n    riseDamping: TIMING.riseDamping,\n    rippleSpeed: TIMING.rippleSpeed,\n    reduced: false,\n  };\n\n  // The grid, rebuilt when the root changes size. Box i sits at column i % cols, row i / cols.\n  let width = 0;\n  let height = 0;\n  let dpr = 1;\n  let cols = 0;\n  let rows = 0;\n  let ox = 0;\n  let oy = 0;\n  let depth = new Float32Array(0); // px behind the surface; negative while bobbing in front of it\n  let speed = new Float32Array(0);\n  const active: number[] = [];\n\n  const pointer = { x: 0, y: 0, inside: false, down: false, press: 0 };\n  const rings: { x: number; y: number; t0: number }[] = [];\n\n  let raf = 0;\n  let last = 0;\n  let visible = false;\n  const cleanups: (() => void)[] = [];\n\n  function readColors() {\n    if (!probes) return;\n    const el = probes.face;\n    const face = getComputedStyle(el).color;\n    const accent = tinted ? getComputedStyle(probes.accent).color : null;\n    const dark = isDark(face);\n    for (const name of FACE_NAMES) {\n      const f: Shading = FACES[name];\n      const { base, deep } = (dark && f.dark) || f;\n      palette[name] = [];\n      for (let lv = 0; lv < SURFACE.levels; lv++) {\n        const q = lv / (SURFACE.levels - 1);\n        const tone = Math.round((base + deep * q) * 100);\n        let color =\n          tone >= 0\n            ? `color-mix(in oklab, ${face}, white ${tone}%)`\n            : `color-mix(in oklab, ${face}, black ${-tone}%)`;\n        // Eased in, so a shallow box or a passing ring barely takes it on and the floor glows.\n        if (accent) color = `color-mix(in oklab, ${color}, ${accent} ${Math.round(f.accent * q * q * 100)}%)`;\n        el.style.color = color;\n        palette[name].push(getComputedStyle(el).color);\n      }\n    }\n    el.style.color = \"\";\n  }\n\n  function layout() {\n    if (!root || !canvas) return;\n    const w = root.clientWidth;\n    const h = root.clientHeight;\n    dpr = Math.min(window.devicePixelRatio || 1, SURFACE.maxDpr);\n    const c = Math.max(8, options.cell);\n    const nextCols = Math.ceil(w / c) + 1;\n    const nextRows = Math.ceil(h / c) + 1;\n    if (w === width && h === height && canvas.width === Math.round(w * dpr) && nextCols === cols && nextRows === rows) {\n      ox = (w - cols * c) / 2;\n      oy = (h - rows * c) / 2;\n      return;\n    }\n    width = w;\n    height = h;\n    canvas.width = Math.round(w * dpr);\n    canvas.height = Math.round(h * dpr);\n    // One box more than fits, centered, so the wall runs off every edge.\n    cols = nextCols;\n    rows = nextRows;\n    ox = (w - cols * c) / 2;\n    oy = (h - rows * c) / 2;\n    depth = new Float32Array(cols * rows);\n    speed = new Float32Array(cols * rows);\n  }\n\n  function step(dt: number, t: number) {\n    const { cell, reach, falloff, reduced } = options;\n    const held = pointer.inside && pointer.down ? 1 : 0;\n    if (reduced) pointer.press = held;\n    else {\n      const tau = held > pointer.press ? TIMING.pressIn : TIMING.pressOut;\n      pointer.press += (held - pointer.press) * (1 - Math.exp(-dt / tau));\n      if (Math.abs(held - pointer.press) < 0.001) pointer.press = held;\n    }\n    let moving = pointer.press !== held;\n\n    const floor = options.depth * (1 + SURFACE.pressBoost * pointer.press);\n    const inner = reach * (1 - clamp01(falloff));\n    const reach2 = reach * reach;\n    // The floor is seen from above, so it shows higher than it sits; aim the dent below the pointer to land it under it.\n    const aimX = pointer.x + options.tilt * 0.3 * options.depth * 0.8;\n    const aimY = pointer.y + options.tilt * options.depth * 0.8;\n\n    // Rings: radius, height and width now; drop the ones that have faded.\n    const ring = SURFACE.ring;\n    const ringWidth = cell * ring.width;\n    const live: { x: number; y: number; r: number; amp: number }[] = [];\n    for (let k = rings.length - 1; k >= 0; k--) {\n      const age = t - rings[k].t0;\n      const amp = options.depth * ring.amp * Math.exp(-age / TIMING.ringDecay);\n      if (amp < 0.3) rings.splice(k, 1);\n      else live.push({ x: rings[k].x, y: rings[k].y, r: age * options.rippleSpeed, amp });\n    }\n    if (live.length) moving = true;\n\n    // Sink on a stiff spring, rise on a soft one.\n    const push = { stiffness: options.sinkStiffness, damping: TIMING.sinkDamping };\n    const release = { stiffness: options.riseStiffness, damping: options.riseDamping };\n    const pushC = 2 * push.damping * Math.sqrt(push.stiffness);\n    const releaseC = 2 * release.damping * Math.sqrt(release.stiffness);\n    const sub = 2;\n    const h = dt / sub;\n\n    for (let i = 0; i < depth.length; i++) {\n      const x = ox + ((i % cols) + 0.5) * cell;\n      const y = oy + (((i / cols) | 0) + 0.5) * cell;\n\n      let target = 0;\n      if (pointer.inside) {\n        const dx = x - aimX;\n        const dy = y - aimY;\n        const r2 = dx * dx + dy * dy;\n        if (r2 < reach2) {\n          const r = Math.sqrt(r2);\n          if (r <= inner) target = floor;\n          else {\n            const u = (reach - r) / (reach - inner);\n            target = floor * u * u * (3 - 2 * u);\n          }\n        }\n      }\n      for (const rg of live) {\n        const u = (Math.hypot(x - rg.x, y - rg.y) - rg.r) / ringWidth;\n        if (u > -3 && u < 3) target += rg.amp * Math.exp(-u * u);\n      }\n\n      if (reduced) {\n        depth[i] = target;\n        speed[i] = 0;\n        continue;\n      }\n\n      let d = depth[i];\n      let v = speed[i];\n      if (d === target && v === 0) continue;\n      const sinking = target > d && target > SURFACE.rest;\n      const k = sinking ? push.stiffness : release.stiffness;\n      const c = sinking ? pushC : releaseC;\n      for (let s = 0; s < sub; s++) {\n        v += (k * (target - d) - c * v) * h;\n        d += v * h;\n      }\n      if (Math.abs(target - d) < SURFACE.rest && Math.abs(v) < SURFACE.rest * 10) {\n        d = target;\n        v = 0;\n      } else moving = true;\n      depth[i] = d;\n      speed[i] = v;\n    }\n    return moving;\n  }\n\n  function draw() {\n    if (!ctx) return;\n    const { cell, gap, perspective, tilt } = options;\n    const cx = width / 2;\n    const cy = height / 2;\n    const floor = options.depth * (1 + SURFACE.pressBoost);\n    const top = SURFACE.levels - 1;\n    const level = (d: number) => (d <= 0 ? 0 : Math.min(top, Math.round((d / floor) * top)));\n    // Seen from a little above and to the left, in perspective around the center.\n    const shiftX = -tilt * 0.3;\n    const shiftY = -tilt;\n    const px = (x: number, d: number) => cx + (x - cx) * (perspective / (perspective + d)) + shiftX * d;\n    const py = (y: number, d: number) => cy + (y - cy) * (perspective / (perspective + d)) + shiftY * d;\n    const inset = Math.min(gap, cell / 2) / 2;\n    const n = depth.length;\n\n    // A wall of box i shows where a neighbor sits deeper, from this box's front back to\n    // the neighbor's. Each is drawn only if it faces the viewer.\n    const wall = (face: Face, ax: number, ay: number, bx: number, by: number, d: number, dn: number) => {\n      ctx!.fillStyle = palette[face][level((d + dn) / 2)];\n      ctx!.beginPath();\n      ctx!.moveTo(px(ax, d), py(ay, d));\n      ctx!.lineTo(px(bx, d), py(by, d));\n      ctx!.lineTo(px(bx, dn), py(by, dn));\n      ctx!.lineTo(px(ax, dn), py(ay, dn));\n      ctx!.fill();\n    };\n    const walls = (i: number) => {\n      const col = i % cols;\n      const row = (i / cols) | 0;\n      const d = depth[i];\n      const x0 = ox + col * cell;\n      const x1 = x0 + cell;\n      const y0 = oy + row * cell;\n      const y1 = y0 + cell;\n      const left = col > 0 ? depth[i - 1] : 0;\n      const right = col < cols - 1 ? depth[i + 1] : 0;\n      const up = row > 0 ? depth[i - cols] : 0;\n      const down = row < rows - 1 ? depth[i + cols] : 0;\n      if (up > d && py(y0, up) < py(y0, d)) wall(\"up\", x0, y0, x1, y0, d, up);\n      if (down > d && py(y1, down) > py(y1, d)) wall(\"down\", x0, y1, x1, y1, d, down);\n      if (left > d && px(x0, left) < px(x0, d)) wall(\"left\", x0, y0, x0, y1, d, left);\n      if (right > d && px(x1, right) > px(x1, d)) wall(\"right\", x1, y0, x1, y1, d, right);\n    };\n    const front = (i: number) => {\n      const col = i % cols;\n      const row = (i / cols) | 0;\n      const d = depth[i];\n      // Shade a sunk box more under a step above or to its left, where the light is blocked.\n      const up = row > 0 ? depth[i - cols] : 0;\n      const left = col > 0 ? depth[i - 1] : 0;\n      const shade = level(d + Math.max(0, d - Math.min(up, left)) * SURFACE.shadow);\n      const x0 = ox + col * cell;\n      const y0 = oy + row * cell;\n      const sx0 = px(x0, d);\n      const sy0 = py(y0, d);\n      const sx1 = px(x0 + cell, d);\n      const sy1 = py(y0 + cell, d);\n      ctx!.fillStyle = palette.seam[shade];\n      ctx!.fillRect(sx0, sy0, sx1 - sx0, sy1 - sy0);\n      const k = (sx1 - sx0) / cell;\n      ctx!.fillStyle = palette.front[shade];\n      ctx!.fillRect(sx0 + inset * k, sy0 + inset * k, sx1 - sx0 - 2 * inset * k, sy1 - sy0 - 2 * inset * k);\n    };\n\n    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    ctx.clearRect(0, 0, width, height);\n\n    // Painter's order: the deepest boxes first, then the surface, then any box bobbing in front of it.\n    active.length = 0;\n    for (let i = 0; i < n; i++) if (depth[i] !== 0) active.push(i);\n    active.sort((a, b) => depth[b] - depth[a]);\n    let k = 0;\n    for (; k < active.length && depth[active[k]] > 0; k++) {\n      walls(active[k]);\n      front(active[k]);\n    }\n\n    if (active.length) for (let i = 0; i < n; i++) if (depth[i] === 0) walls(i);\n    ctx.fillStyle = palette.seam[0];\n    ctx.beginPath();\n    for (let i = 0; i < n; i++) {\n      if (depth[i] !== 0) continue;\n      ctx.rect(ox + (i % cols) * cell, oy + ((i / cols) | 0) * cell, cell, cell);\n    }\n    ctx.fill();\n    ctx.fillStyle = palette.front[0];\n    ctx.beginPath();\n    for (let i = 0; i < n; i++) {\n      if (depth[i] !== 0) continue;\n      const col = i % cols;\n      const row = (i / cols) | 0;\n      ctx.rect(ox + col * cell + inset, oy + row * cell + inset, cell - inset * 2, cell - inset * 2);\n    }\n    ctx.fill();\n\n    for (; k < active.length; k++) {\n      walls(active[k]);\n      front(active[k]);\n    }\n  }\n\n  function frame(time: number) {\n    raf = 0;\n    const t = time / 1000;\n    const dt = last ? Math.min(t - last, 1 / 30) : 1 / 60;\n    last = t;\n    const moving = step(dt, t);\n    draw();\n    if (moving && visible) raf = requestAnimationFrame(frame);\n    else last = 0;\n  }\n\n  function wake() {\n    if (!raf && visible && ctx) raf = requestAnimationFrame(frame);\n  }\n\n  return {\n    connect(el: { root: HTMLElement; canvas: HTMLCanvasElement; face: HTMLElement; accent: HTMLElement }) {\n      root = el.root;\n      canvas = el.canvas;\n      ctx = canvas.getContext(\"2d\");\n      probes = { face: el.face, accent: el.accent };\n      readColors();\n      width = 0;\n      layout();\n\n      const resize = new ResizeObserver(() => {\n        layout();\n        wake();\n      });\n      resize.observe(root);\n      const io = new IntersectionObserver(([entry]) => {\n        visible = entry.isIntersecting;\n        if (visible) wake();\n      });\n      io.observe(root);\n\n      const scheme = window.matchMedia(\"(prefers-color-scheme: dark)\");\n      const recolor = () => {\n        readColors();\n        wake();\n      };\n      scheme.addEventListener(\"change\", recolor);\n      const themeAttrs = new MutationObserver(recolor);\n      themeAttrs.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"data-theme\", \"style\"] });\n\n      const locate = (e: PointerEvent) => {\n        const r = root!.getBoundingClientRect();\n        pointer.x = e.clientX - r.left;\n        pointer.y = e.clientY - r.top;\n      };\n      const move = (e: PointerEvent) => {\n        // A finger only presses while it's down.\n        if (e.pointerType === \"touch\" && !pointer.down) return;\n        locate(e);\n        pointer.inside = true;\n        wake();\n      };\n      const down = (e: PointerEvent) => {\n        locate(e);\n        pointer.inside = true;\n        pointer.down = true;\n        if (options.ripple && !options.reduced) rings.push({ x: pointer.x, y: pointer.y, t0: performance.now() / 1000 });\n        wake();\n      };\n      const up = (e: PointerEvent) => {\n        pointer.down = false;\n        if (e.pointerType === \"touch\") pointer.inside = false;\n        wake();\n      };\n      const leave = () => {\n        pointer.inside = false;\n        pointer.down = false;\n        wake();\n      };\n      root.addEventListener(\"pointermove\", move);\n      root.addEventListener(\"pointerdown\", down);\n      window.addEventListener(\"pointerup\", up);\n      root.addEventListener(\"pointerleave\", leave);\n      root.addEventListener(\"pointercancel\", leave);\n\n      cleanups.push(\n        () => resize.disconnect(),\n        () => io.disconnect(),\n        () => scheme.removeEventListener(\"change\", recolor),\n        () => themeAttrs.disconnect(),\n        () => {\n          root?.removeEventListener(\"pointermove\", move);\n          root?.removeEventListener(\"pointerdown\", down);\n          window.removeEventListener(\"pointerup\", up);\n          root?.removeEventListener(\"pointerleave\", leave);\n          root?.removeEventListener(\"pointercancel\", leave);\n        },\n      );\n      return () => {\n        cancelAnimationFrame(raf);\n        raf = 0;\n        last = 0;\n        cleanups.splice(0).forEach((fn) => fn());\n        root = canvas = null;\n        ctx = null;\n        probes = null;\n      };\n    },\n\n    setOptions(next: Options) {\n      const regrid = next.cell !== options.cell;\n      options = next;\n      if (regrid) {\n        cols = rows = 0;\n        layout();\n      }\n      wake();\n      // Draw once even when nothing moves, so a new size or color shows.\n      if (ctx && !raf) draw();\n    },\n\n    recolor(accent: boolean) {\n      tinted = accent;\n      readColors();\n      if (ctx && !raf) draw();\n    },\n  };\n}\n\nexport interface ImprintProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Size of one box, in px. */\n  cell?: number;\n  /** Seam between boxes, in px. */\n  gap?: number;\n  /** How far the boxes under the pointer sink, in px. */\n  depth?: number;\n  /** Radius of the dent around the pointer, in px. */\n  reach?: number;\n  /** Shape of the dent, from 0 (one flat floor with a sharp rim) to 1 (a smooth bowl). */\n  falloff?: number;\n  /** How far above the wall the viewer sits. Higher shows more of the walls. `0` looks straight on. */\n  tilt?: number;\n  /** Distance from the viewer to the wall, in px. Lower is a stronger perspective. */\n  perspective?: number;\n  /** Roll a ring out across the wall from every press. */\n  ripple?: boolean;\n  /** Stiffness of the spring a box sinks on. Higher snaps the dent in under the pointer faster. */\n  sinkStiffness?: number;\n  /** Stiffness of the spring a box rises back on. Lower fills the dent in more slowly, for a longer trail. */\n  riseStiffness?: number;\n  /** Damping ratio of the rise. Under `1` a box bobs past the surface before it settles; `1` and up comes back without it. */\n  riseDamping?: number;\n  /** How fast the ring from a press rolls out across the wall, in px per second. */\n  rippleSpeed?: number;\n  /** Classes that set the boxes' color (a `text-*` color). Walls and floor are shaded from it. */\n  boxClassName?: string;\n  /** Classes that tint the floor of the dent (a `text-*` color). No tint when unset. */\n  accentClassName?: string;\n  /** Classes for the canvas, e.g. an opacity or a mask. */\n  canvasClassName?: string;\n}\n\nexport function Imprint({\n  cell = 48,\n  gap = 2,\n  depth = 48,\n  reach = 140,\n  falloff = 0.45,\n  tilt = 0.35,\n  perspective = 900,\n  ripple = true,\n  sinkStiffness = TIMING.sinkStiffness,\n  riseStiffness = TIMING.riseStiffness,\n  riseDamping = TIMING.riseDamping,\n  rippleSpeed = TIMING.rippleSpeed,\n  boxClassName,\n  accentClassName,\n  canvasClassName,\n  className,\n  children,\n  ...props\n}: ImprintProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const faceRef = React.useRef<HTMLSpanElement>(null);\n  const accentRef = React.useRef<HTMLSpanElement>(null);\n  const [imprint] = React.useState(createImprint);\n  const reduced = useReducedMotion() ?? false;\n\n  React.useLayoutEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    const face = faceRef.current;\n    const accent = accentRef.current;\n    if (!root || !canvas || !face || !accent) return;\n    return imprint.connect({ root, canvas, face, accent });\n  }, [imprint]);\n\n  React.useEffect(() => {\n    imprint.setOptions({\n      cell,\n      gap,\n      depth,\n      reach,\n      falloff,\n      tilt,\n      perspective,\n      ripple,\n      sinkStiffness,\n      riseStiffness,\n      riseDamping,\n      rippleSpeed,\n      reduced,\n    });\n  }, [\n    imprint,\n    cell,\n    gap,\n    depth,\n    reach,\n    falloff,\n    tilt,\n    perspective,\n    ripple,\n    sinkStiffness,\n    riseStiffness,\n    riseDamping,\n    rippleSpeed,\n    reduced,\n  ]);\n\n  React.useEffect(() => {\n    imprint.recolor(Boolean(accentClassName));\n  }, [imprint, boxClassName, accentClassName]);\n\n  return (\n    <div ref={rootRef} className={cn(\"relative isolate overflow-hidden\", className)} {...props}>\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        className={cn(\"pointer-events-none absolute inset-0 -z-10 size-full\", canvasClassName)}\n      />\n      {/* Probes the canvas reads its colors from. The default applies only when no class is given, so a `dark:` default can't win over yours. */}\n      <span ref={faceRef} aria-hidden className={cn(\"hidden\", boxClassName ?? \"text-zinc-100 dark:text-zinc-900\")} />\n      <span ref={accentRef} aria-hidden className={cn(\"hidden\", accentClassName)} />\n      {children}\n    </div>\n  );\n}\n"
    }
  ]
}