{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "compare-crack-seam",
  "title": "Compare Crack Seam",
  "description": "Before/after comparison slider whose divider is a living Voronoi crack seam — fast drags spawn branching micro-fissures, slow drags heal them shut, and release settles with a 1px specular glint traveling the fracture.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/compare-crack-seam/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, type ReactNode } from \"react\";\n\ntype Pt = { x: number; y: number };\ntype Fissure = {\n  /** origin x relative to the handle (seam offset at spawn) */\n  ox: number;\n  /** origin y in pane coordinates */\n  oy: number;\n  /** polyline relative to the origin */\n  pts: Pt[];\n  len: number;\n  alpha: number;\n  born: number;\n  /** timestamp retraction started; 0 while still held open */\n  retract: number;\n};\n\nconst HANDLE_W = 40;\nconst GROW_MS = 90;\nconst RETRACT_MS = 300;\nconst GLINT_MS = 450;\nconst GLINT_LEN = 56;\nconst SPRING_K = 170; // s^-2\nconst SPRING_C = 2 * 0.85 * Math.sqrt(SPRING_K); // zeta 0.85 — taut, no wobble\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\nconst easeOut = (t: number) => 1 - (1 - t) ** 3;\n\nfunction parseColor(raw: string): { r: number; g: number; b: number } | null {\n  const v = raw.trim();\n  if (v.startsWith(\"#\")) {\n    const hex = v.slice(1);\n    if (hex.length === 3) {\n      const r = parseInt(hex[0] + hex[0], 16);\n      const g = parseInt(hex[1] + hex[1], 16);\n      const b = parseInt(hex[2] + hex[2], 16);\n      if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;\n      return { r, g, b };\n    }\n    if (hex.length >= 6) {\n      const r = parseInt(hex.slice(0, 2), 16);\n      const g = parseInt(hex.slice(2, 4), 16);\n      const b = parseInt(hex.slice(4, 6), 16);\n      if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;\n      return { r, g, b };\n    }\n    return null;\n  }\n  const m = v.match(/rgba?\\(\\s*([\\d.]+)[,\\s]+([\\d.]+)[,\\s]+([\\d.]+)/i);\n  if (!m) return null;\n  return { r: Number(m[1]), g: Number(m[2]), b: Number(m[3]) };\n}\n\n/** dart-throwing Poisson disc — relaxes min distance if the pane is crowded */\nfunction poissonPoints(w: number, h: number, count: number, minDist: number): Pt[] {\n  const pts: Pt[] = [];\n  let d = minDist;\n  let attempts = 0;\n  while (pts.length < count && attempts < 6000) {\n    attempts++;\n    const c = { x: Math.random() * w, y: Math.random() * h };\n    let ok = true;\n    for (const q of pts) {\n      const dx = q.x - c.x;\n      const dy = q.y - c.y;\n      if (dx * dx + dy * dy < d * d) {\n        ok = false;\n        break;\n      }\n    }\n    if (ok) pts.push(c);\n    if (attempts % 1500 === 0) d *= 0.85;\n  }\n  return pts;\n}\n\n/**\n * Nearest and second-nearest seeds to (x, y). Their perpendicular bisector is\n * the half-plane boundary between the two Voronoi cells — the exact wall the\n * confirm-slide-shatter clipper would produce — so points solved on it lie on real\n * cell edges without ever chaining polygons.\n */\nfunction nearestPair(seeds: Pt[], x: number, y: number): { a: Pt; b: Pt; ia: number } {\n  let ia = 0;\n  let ib = 1;\n  let da = Infinity;\n  let db = Infinity;\n  for (let i = 0; i < seeds.length; i++) {\n    const s = seeds[i];\n    const d = (s.x - x) * (s.x - x) + (s.y - y) * (s.y - y);\n    if (d < da) {\n      db = da;\n      ib = ia;\n      da = d;\n      ia = i;\n    } else if (d < db) {\n      db = d;\n      ib = i;\n    }\n  }\n  return { a: seeds[ia], b: seeds[ib], ia };\n}\n\n// Before/after comparison slider whose divider is a living Voronoi crack seam.\n// The seam polyline rides the cell walls nearest the handle, re-jags only when\n// the handle crosses a cell, splinters micro-fissures on fast drags, heals\n// them on slow ones, and fires a 1px specular glint down the fracture on\n// release. Clip-path + canvas are written direct-DOM in one rAF loop that\n// sleeps at rest.\nexport function CrackCompare({\n  before = <div className=\"h-full w-full bg-surface\" />,\n  after = <div className=\"h-full w-full bg-background\" />,\n  initial = 0.5,\n  seedCount = 140,\n  jag = 18,\n  spawnVelocity = 600,\n  label = \"Comparison position\",\n  onChange,\n  className = \"\",\n}: {\n  /** left layer — revealed as the seam moves right */\n  before?: ReactNode;\n  /** right layer — clipped along the crack seam */\n  after?: ReactNode;\n  /** initial split position 0..1 */\n  initial?: number;\n  /** Voronoi seed count feeding the seam's cell walls */\n  seedCount?: number;\n  /** max horizontal jag of the seam in px */\n  jag?: number;\n  /** |vx| in px/s above which micro-fissures branch off the seam */\n  spawnVelocity?: number;\n  /** aria label for the hidden native range input */\n  label?: string;\n  /** fires on release and keyboard change with the split 0..1 */\n  onChange?: (value: number) => void;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const afterRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const dividerRef = useRef<HTMLDivElement>(null);\n  const handleRef = useRef<HTMLDivElement>(null);\n  const gripRef = useRef<HTMLDivElement>(null);\n  const rangeRef = useRef<HTMLInputElement>(null);\n  const onChangeRef = useRef(onChange);\n  onChangeRef.current = onChange;\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const afterEl = afterRef.current;\n    const canvas = canvasRef.current;\n    const divider = dividerRef.current;\n    const handle = handleRef.current;\n    const grip = gripRef.current;\n    const range = rangeRef.current;\n    if (!root || !afterEl || !canvas || !divider || !handle || !grip || !range) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let w = 0;\n    let h = 0;\n    let prevW = 0;\n    let seeds: Pt[] = [];\n    let ys: number[] = [];\n\n    const S = {\n      x: 0,\n      v: 0,\n      target: 0,\n      dragging: false,\n      raf: 0,\n      last: 0,\n      rectLeft: 0,\n      cell: -1,\n      offsets: [] as number[],\n      goals: [] as number[],\n      fissures: [] as Fissure[],\n      lastSpawn: 0,\n      pendingGlint: false,\n      glintStart: -1,\n      seamRGB: { r: 255, g: 255, b: 255 },\n    };\n\n    /** seam x-offsets (relative to the handle) solved on the local Voronoi walls */\n    const seamGoalsFor = (x: number): number[] =>\n      ys.map((y) => {\n        if (seeds.length < 2) return 0;\n        const { a, b } = nearestPair(seeds, x, y);\n        const dx = b.x - a.x;\n        if (Math.abs(dx) < 0.001) return 0;\n        // solve px on the a|b bisector at this y: |p-a| = |p-b|\n        const px =\n          (b.x * b.x - a.x * a.x + (a.y - b.y) * (2 * y - a.y - b.y)) / (2 * dx);\n        return clamp(px - x, -jag, jag);\n      });\n\n    const cellAt = (x: number) =>\n      seeds.length < 2 ? 0 : nearestPair(seeds, x, h / 2).ia;\n\n    const seamPts = (): Pt[] => ys.map((y, i) => ({ x: S.x + S.offsets[i], y }));\n\n    const syncRange = () => {\n      const pct = Math.round((S.x / Math.max(1, w)) * 100);\n      if (document.activeElement === range && !S.dragging) return;\n      if (range.value !== String(pct)) range.value = String(pct);\n    };\n\n    /** stroke a fissure polyline up to maxLen px of arc length */\n    const strokePartial = (ox: number, oy: number, pts: Pt[], maxLen: number) => {\n      ctx.moveTo(ox, oy);\n      let prev: Pt = { x: 0, y: 0 };\n      let remaining = maxLen;\n      for (const p of pts) {\n        const seg = Math.hypot(p.x - prev.x, p.y - prev.y);\n        if (seg >= remaining) {\n          const f = remaining / seg;\n          ctx.lineTo(ox + prev.x + (p.x - prev.x) * f, oy + prev.y + (p.y - prev.y) * f);\n          return;\n        }\n        ctx.lineTo(ox + p.x, oy + p.y);\n        prev = p;\n        remaining -= seg;\n      }\n    };\n\n    const pointAt = (pts: Pt[], cum: number[], s: number): Pt => {\n      for (let i = 1; i < pts.length; i++) {\n        if (cum[i] >= s) {\n          const f = (s - cum[i - 1]) / (cum[i] - cum[i - 1] || 1);\n          return {\n            x: pts[i - 1].x + (pts[i].x - pts[i - 1].x) * f,\n            y: pts[i - 1].y + (pts[i].y - pts[i - 1].y) * f,\n          };\n        }\n      }\n      return pts[pts.length - 1];\n    };\n\n    /** 1px specular dash traveling the settled seam over 450ms */\n    const drawGlint = (now: number, pts: Pt[]) => {\n      const p = (now - S.glintStart) / GLINT_MS;\n      if (p >= 1) {\n        S.glintStart = -1;\n        return;\n      }\n      const cum: number[] = [0];\n      let total = 0;\n      for (let i = 1; i < pts.length; i++) {\n        total += Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y);\n        cum.push(total);\n      }\n      const raw = easeOut(p) * (total + GLINT_LEN);\n      const head = clamp(raw, 0, total);\n      const tail = clamp(raw - GLINT_LEN, 0, total);\n      if (head - tail < 1) return;\n      ctx.beginPath();\n      const t0 = pointAt(pts, cum, tail);\n      ctx.moveTo(t0.x, t0.y);\n      for (let i = 1; i < pts.length; i++) {\n        if (cum[i] > tail && cum[i] < head) ctx.lineTo(pts[i].x, pts[i].y);\n      }\n      const t1 = pointAt(pts, cum, head);\n      ctx.lineTo(t1.x, t1.y);\n      const { r, g, b } = S.seamRGB;\n      ctx.lineWidth = 1;\n      ctx.strokeStyle = `rgba(${r},${g},${b},0.5)`;\n      ctx.stroke();\n    };\n\n    const draw = (now: number, pts: Pt[]) => {\n      ctx.clearRect(0, 0, w, h);\n      if (pts.length < 2) return;\n      // seam: hairline + offset ghost stroke for glass depth — token-derived, theme-aware\n      const { r, g, b } = S.seamRGB;\n      const passes = [\n        { o: 0.75, lw: 0.5, c: `rgba(${r},${g},${b},0.10)` },\n        { o: 0, lw: 1, c: `rgba(${r},${g},${b},0.32)` },\n      ];\n      for (const pass of passes) {\n        ctx.beginPath();\n        ctx.moveTo(pts[0].x + pass.o, pts[0].y + pass.o);\n        for (let i = 1; i < pts.length; i++) {\n          ctx.lineTo(pts[i].x + pass.o, pts[i].y + pass.o);\n        }\n        ctx.lineWidth = pass.lw;\n        ctx.strokeStyle = pass.c;\n        ctx.stroke();\n      }\n      // micro-fissures: grow fast, retract over 300ms ease-out once slow\n      for (const f of S.fissures) {\n        const grow = easeOut(Math.min(1, (now - f.born) / GROW_MS));\n        const heal = f.retract\n          ? 1 - easeOut(Math.min(1, (now - f.retract) / RETRACT_MS))\n          : 1;\n        const drawn = f.len * grow * heal;\n        if (drawn < 0.5) continue;\n        ctx.beginPath();\n        strokePartial(S.x + f.ox, f.oy, f.pts, drawn);\n        ctx.lineWidth = 0.75;\n        ctx.strokeStyle = `rgba(${r},${g},${b},${(f.alpha * heal).toFixed(3)})`;\n        ctx.stroke();\n      }\n      if (S.glintStart >= 0) drawGlint(now, pts);\n    };\n\n    const applyStatic = () => {\n      afterEl.style.clipPath = `inset(0 0 0 ${S.x.toFixed(1)}px)`;\n      divider.style.transform = `translateX(${S.x.toFixed(1)}px)`;\n      handle.style.transform = `translateX(${(S.x - HANDLE_W / 2).toFixed(2)}px)`;\n      syncRange();\n    };\n\n    const applyFrame = (now: number) => {\n      if (ys.length < 2) return;\n      const pts = seamPts();\n      let poly = \"\";\n      for (const p of pts) poly += `${p.x.toFixed(1)}px ${p.y.toFixed(1)}px, `;\n      afterEl.style.clipPath = `polygon(${poly}${(w + 40).toFixed(0)}px ${(h + 6).toFixed(0)}px, ${(w + 40).toFixed(0)}px -6px)`;\n      handle.style.transform = `translateX(${(S.x - HANDLE_W / 2).toFixed(2)}px)`;\n      draw(now, pts);\n    };\n\n    const spawnFissures = (now: number, speed: number) => {\n      S.lastSpawn = now;\n      const n = 2 + Math.floor(Math.random() * 3); // 2–4 branches\n      const alpha = clamp(0.3 + (speed - spawnVelocity) / 1800, 0.3, 0.85);\n      for (let i = 0; i < n && S.fissures.length < 28; i++) {\n        const idx = 1 + Math.floor(Math.random() * Math.max(1, ys.length - 2));\n        const oy = ys[idx] + (Math.random() - 0.5) * 10;\n        const ox = S.offsets[idx] ?? 0;\n        const dir = Math.random() < 0.5 ? -1 : 1;\n        const len = 30 + Math.random() * 40;\n        const a0 = Math.random() * 0.9 - 0.45 + (dir < 0 ? Math.PI : 0);\n        const a1 = a0 + (Math.random() * 0.8 - 0.4);\n        const l0 = len * (0.4 + Math.random() * 0.3);\n        const p1 = { x: Math.cos(a0) * l0, y: Math.sin(a0) * l0 };\n        const p2 = { x: p1.x + Math.cos(a1) * (len - l0), y: p1.y + Math.sin(a1) * (len - l0) };\n        S.fissures.push({ ox, oy, pts: [p1, p2], len, alpha, born: now, retract: 0 });\n      }\n    };\n\n    const loop = (now: number) => {\n      const dt = Math.min((now - S.last) / 1000, 1 / 30);\n      S.last = now;\n\n      // taut spring toward the pointer / keyboard target\n      S.v += (SPRING_K * (S.target - S.x) - SPRING_C * S.v) * dt;\n      S.x = clamp(S.x + S.v * dt, 0, w);\n      const speed = Math.abs(S.v);\n\n      // seam re-jags only when the handle crosses into a new Voronoi cell\n      const cell = cellAt(S.x);\n      if (cell !== S.cell) {\n        S.cell = cell;\n        S.goals = seamGoalsFor(S.x);\n      }\n      const k = 1 - Math.exp(-16 * dt);\n      let seamMoving = false;\n      for (let i = 0; i < S.offsets.length; i++) {\n        S.offsets[i] += (S.goals[i] - S.offsets[i]) * k;\n        if (Math.abs(S.goals[i] - S.offsets[i]) > 0.25) seamMoving = true;\n      }\n\n      // fast drags splinter, slow drags heal\n      if (S.dragging && speed > spawnVelocity && now - S.lastSpawn > 70) {\n        spawnFissures(now, speed);\n      }\n      if (speed < spawnVelocity) {\n        for (const f of S.fissures) if (!f.retract) f.retract = now;\n      }\n      S.fissures = S.fissures.filter(\n        (f) => !(f.retract && now - f.retract >= RETRACT_MS)\n      );\n\n      const settled = !S.dragging && Math.abs(S.target - S.x) < 0.3 && speed < 4;\n      if (settled) {\n        S.x = clamp(S.target, 0, w);\n        S.v = 0;\n        if (S.pendingGlint) {\n          S.pendingGlint = false;\n          S.glintStart = now;\n        }\n      }\n\n      applyFrame(now);\n      syncRange();\n\n      const alive =\n        S.dragging ||\n        !settled ||\n        seamMoving ||\n        S.fissures.length > 0 ||\n        S.glintStart >= 0;\n      S.raf = alive ? requestAnimationFrame(loop) : 0;\n    };\n\n    const wake = () => {\n      if (!S.raf) {\n        S.last = performance.now();\n        S.raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const build = () => {\n      const rect = root.getBoundingClientRect();\n      w = rect.width;\n      h = rect.height;\n      if (w < 4 || h < 4) return;\n      handle.style.left = \"0px\";\n      handle.style.marginLeft = \"0px\";\n      const frac = prevW > 0 ? S.x / prevW : initial;\n      prevW = w;\n      S.x = clamp(frac, 0, 1) * w;\n      S.target = S.x;\n      S.v = 0;\n      S.fissures = [];\n      S.glintStart = -1;\n      S.pendingGlint = false;\n\n      if (reduced) {\n        canvas.style.display = \"none\";\n        divider.style.display = \"\";\n        applyStatic();\n        return;\n      }\n      divider.style.display = \"none\";\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.lineCap = \"round\";\n      ctx.lineJoin = \"round\";\n\n      const cell = Math.sqrt((w * h) / seedCount);\n      seeds = poissonPoints(w, h, seedCount, Math.max(6, cell * 0.72));\n      const step = clamp(cell * 0.55, 14, 48);\n      ys = [];\n      for (let y = -6; y < h + 6; y += step) ys.push(y);\n      ys.push(h + 6);\n\n      S.cell = cellAt(S.x);\n      S.goals = seamGoalsFor(S.x);\n      S.offsets = S.goals.slice();\n      S.last = performance.now();\n      applyFrame(S.last);\n      syncRange();\n    };\n\n    const onDown = (e: PointerEvent) => {\n      handle.setPointerCapture(e.pointerId);\n      S.dragging = true;\n      S.rectLeft = root.getBoundingClientRect().left;\n      S.target = clamp(e.clientX - S.rectLeft, 0, w);\n      if (reduced) {\n        S.x = S.target;\n        applyStatic();\n      } else {\n        wake();\n      }\n    };\n    const onMove = (e: PointerEvent) => {\n      if (!S.dragging) return;\n      S.target = clamp(e.clientX - S.rectLeft, 0, w);\n      if (reduced) {\n        S.x = S.target;\n        applyStatic();\n      }\n    };\n    const onUp = () => {\n      if (!S.dragging) return;\n      S.dragging = false;\n      onChangeRef.current?.(w > 0 ? S.target / w : 0);\n      if (!reduced) {\n        S.pendingGlint = true;\n        wake();\n      }\n    };\n\n    const onRange = () => {\n      S.target = (Number(range.value) / 100) * w;\n      if (reduced) {\n        S.x = S.target;\n        applyStatic();\n      } else {\n        wake();\n      }\n      onChangeRef.current?.(Number(range.value) / 100);\n    };\n    const onFocus = () => {\n      if (range.matches(\":focus-visible\")) {\n        grip.classList.add(\"outline\", \"outline-2\", \"outline-offset-2\", \"outline-ns-accent\");\n      }\n    };\n    const onBlur = () => {\n      grip.classList.remove(\"outline\", \"outline-2\", \"outline-offset-2\", \"outline-ns-accent\");\n    };\n\n    /** re-sample the seam color off the resolved --foreground token so the\n     * crack reads on light or dark content alike, then repaint once even at rest */\n    const deriveSeamColor = () => {\n      const raw = getComputedStyle(document.documentElement).getPropertyValue(\"--foreground\");\n      S.seamRGB = parseColor(raw) ?? S.seamRGB;\n      if (S.raf) return;\n      const pts = seamPts();\n      if (pts.length >= 2) draw(performance.now(), pts);\n    };\n    deriveSeamColor();\n    const themeObserver = new MutationObserver(deriveSeamColor);\n    themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n\n    const ro = new ResizeObserver(build);\n    ro.observe(root);\n    handle.addEventListener(\"pointerdown\", onDown);\n    handle.addEventListener(\"pointermove\", onMove);\n    handle.addEventListener(\"pointerup\", onUp);\n    handle.addEventListener(\"pointercancel\", onUp);\n    range.addEventListener(\"input\", onRange);\n    range.addEventListener(\"focus\", onFocus);\n    range.addEventListener(\"blur\", onBlur);\n    return () => {\n      cancelAnimationFrame(S.raf);\n      ro.disconnect();\n      themeObserver.disconnect();\n      handle.removeEventListener(\"pointerdown\", onDown);\n      handle.removeEventListener(\"pointermove\", onMove);\n      handle.removeEventListener(\"pointerup\", onUp);\n      handle.removeEventListener(\"pointercancel\", onUp);\n      range.removeEventListener(\"input\", onRange);\n      range.removeEventListener(\"focus\", onFocus);\n      range.removeEventListener(\"blur\", onBlur);\n    };\n  }, [initial, seedCount, jag, spawnVelocity]);\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative aspect-[16/10] w-full select-none overflow-hidden rounded-md border border-border bg-background ${className}`}\n    >\n      {/* before layer — full bleed */}\n      <div className=\"absolute inset-0\">{before}</div>\n\n      {/* after layer — clipped along the crack seam */}\n      <div\n        ref={afterRef}\n        className=\"absolute inset-0\"\n        style={{ clipPath: `inset(0 0 0 ${initial * 100}%)` }}\n      >\n        {after}\n      </div>\n\n      {/* hairline crack strokes + glint */}\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        className=\"pointer-events-none absolute inset-0 h-full w-full\"\n      />\n\n      {/* straight divider — reduced-motion fallback only */}\n      <div\n        ref={dividerRef}\n        aria-hidden\n        className=\"pointer-events-none absolute inset-y-0 left-0 w-px bg-border\"\n        style={{ display: \"none\" }}\n      />\n\n      {/* hidden native range for a11y — arrows move 2% per press */}\n      <input\n        ref={rangeRef}\n        type=\"range\"\n        min={0}\n        max={100}\n        step={2}\n        defaultValue={Math.round(initial * 100)}\n        aria-label={label}\n        className=\"peer sr-only\"\n      />\n\n      {/* drag handle riding the seam */}\n      <div\n        ref={handleRef}\n        aria-hidden\n        className=\"absolute inset-y-0 flex cursor-ew-resize touch-none items-center justify-center will-change-transform\"\n        style={{ left: `${initial * 100}%`, width: HANDLE_W, marginLeft: -HANDLE_W / 2 }}\n      >\n        <div\n          ref={gripRef}\n          className=\"flex h-7 w-7 items-center justify-center rounded-sm border border-foreground/15 bg-foreground/10 text-ns-muted shadow-[inset_0_1px_0_0_color-mix(in_srgb,var(--foreground)_18%,transparent),0_2px_8px_-2px_rgba(0,0,0,0.5)] backdrop-blur-md transition-colors duration-150 hover:border-foreground/30 hover:text-foreground\"\n        >\n          <svg\n            width=\"14\"\n            height=\"14\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            aria-hidden\n          >\n            <path d=\"m9 18-6-6 6-6\" />\n            <path d=\"m15 6 6 6-6 6\" />\n          </svg>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/compare-crack-seam.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "compare",
      "slider",
      "before-after",
      "voronoi",
      "crack",
      "canvas",
      "clip-path",
      "physics",
      "image-diff"
    ],
    "instruction": "A before/after comparison slider whose divider is a living crack seam instead of a straight line. Two absolutely-stacked DOM layers (arbitrary before/after children) sit in a rounded-md border-border frame; the after layer is clipped by a CSS clip-path polygon that follows a jagged seam polyline, and a single DPR-aware canvas overlay draws the hairline crack strokes on top. Seam geometry: seed ~140 Poisson-disc points across the pane; at sample rows spaced ~0.55 cell-heights apart, find the nearest and second-nearest seeds to (handleX, y) and solve the point on their perpendicular bisector — an exact Voronoi cell wall from half-plane math — clamped to ±18px of the handle, giving x-offsets stored relative to the handle so the seam translates with it and only re-jags when the handle crosses into a new cell (nearest-seed id change), with new offsets eased in exponentially. Motion: handle x follows the pointer through a taut spring (k=170 s⁻², zeta=0.85, no wobble) integrated semi-implicitly in one direct-DOM rAF loop that writes the clip-path polygon string, the handle transform, and all canvas strokes with zero React state on the hot path and sleeps whenever settled. While dragging with |vx| > 600 px/s, spawn 2–4 micro-fissure branches (30–70px two-segment polylines, stroke alpha mapped to velocity, capped at 28) anchored to seam offsets; once speed drops below threshold each fissure retracts tip-first over 300ms ease-out. On pointer release the seam settles on the same spring, then a 1px specular dash ~56px long travels the full seam arc-length over 450ms. Seam strokes are token-derived, not hardcoded: the RGB is read off the resolved --foreground CSS custom property on mount and re-sampled via a MutationObserver on documentElement class changes, so the crack, its ghost pass, and the glint stay visible against both light and dark before/after content — 1px 0.32-alpha hairline plus a 0.75px-offset 0.5px 0.10-alpha ghost pass. Interaction: pointer capture on a 40px-wide full-height handle strip with a 28px glass grip chip (rounded-sm, border/background/shadow all color-mix'd off --foreground so the glass affordance reads on light and dark alike, backdrop-blur), a hidden native range input (sr-only, step 2) so arrow keys move 2% per press with a focus-visible accent outline echoed onto the grip, onChange fired on release/keyboard only, and a ResizeObserver rebuild preserving the split fraction. prefers-reduced-motion drops the canvas and spring entirely for a straight 1px bg-border divider with standard instant compare behavior. Zero dependencies.",
    "rank": 2
  },
  "type": "registry:ui"
}