{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "Shredder-TS-TW",
	"title": "Shredder",
	"description": "A list with a paper shredder at the bottom. Drag a row into the slit and the rollers tug it in, pull it through and cut it into strips that curl out underneath, tumble away and fade out. The rest of the list settles down on a spring and the shredded item is handed to you to delete.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "Shredder/Shredder.tsx",
			"content": "'use client';\n\nimport {\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent,\n  type PointerEvent,\n  type ReactNode\n} from 'react';\nimport { flushSync } from 'react-dom';\n\nconst FOLLOW = 30;\nconst SETTLE = 16;\nconst TUG = 2.4;\nconst TUG_DECAY = 0.14;\nconst SLIT = 6;\nconst OVER = 40;\nconst SLIVER = 2;\nconst STACK_K = 320;\nconst STACK_C = 22;\nconst STAGGER = 0.035;\nconst ENTER = 22;\nconst HYSTERESIS = 8;\nconst DEG = Math.PI / 180;\n\nexport interface ShredderItem {\n  id: string | number;\n}\n\nexport interface ShredderProps<T extends ShredderItem> {\n  items?: T[];\n  renderItem: (item: T, index: number) => ReactNode;\n  onShred?: (item: T) => void;\n  onReorder?: (items: T[]) => void;\n  width?: number;\n  height?: number;\n  inset?: number;\n  gap?: number;\n  slitHeight?: number;\n  fallHeight?: number;\n  feedSpeed?: number;\n  bite?: number;\n  autoFeed?: boolean;\n  stripWidth?: number;\n  curl?: number;\n  autoAnimate?: boolean;\n  loop?: boolean;\n  loopAfterDelete?: boolean;\n  dragTilt?: number;\n  lift?: number;\n  slitColor?: string;\n  color?: string;\n  disabled?: boolean;\n  className?: string;\n}\n\ninterface Config<T> {\n  items: T[];\n  height: number;\n  inset: number;\n  gap: number;\n  slitHeight: number;\n  fallHeight: number;\n  feedSpeed: number;\n  bite: number;\n  autoFeed: boolean;\n  stripWidth: number;\n  curl: number;\n  loop: boolean;\n  loopAfterDelete: boolean;\n  dragTilt: number;\n  lift: number;\n  onShred?: (item: T) => void;\n  onReorder?: (items: T[]) => void;\n  disabled: boolean;\n}\n\ninterface Metrics {\n  left: number;\n  top: number;\n  k: number;\n  rw: number;\n  lip: number;\n  exit: number;\n}\n\ninterface Texture {\n  tex: HTMLCanvasElement;\n  scale: number;\n}\n\ninterface Drag<T> {\n  key: string | number;\n  item: T;\n  el: HTMLDivElement;\n  slot: HTMLLIElement;\n  W: number;\n  H: number;\n  rx: number;\n  ry: number;\n  tx: number;\n  ty: number;\n  vx: number;\n  vy: number;\n  tilt: number;\n  lift: number;\n  gx: number;\n  gy: number;\n  px: number;\n  py: number;\n  ox: number;\n  oy: number;\n  moved: boolean;\n  from: number;\n  j: number;\n  phase: 'drag' | 'carry' | 'return';\n  id: number | null;\n  snap: Promise<Texture>;\n  fill: string;\n}\n\ninterface Feed<T> {\n  key: string | number;\n  item: T;\n  el: HTMLDivElement;\n  slot: HTMLLIElement;\n  W: number;\n  H: number;\n  rx: number;\n  ry: number;\n  tx: number;\n  tilt: number;\n  lift: number;\n  v: number;\n  age: number;\n  tex: HTMLCanvasElement | null;\n  ts: number;\n  consumed: boolean;\n  strips: number;\n}\n\ninterface Strip<T> {\n  feed: Feed<T>;\n  x: number;\n  w: number;\n  H: number;\n  phase: 'attached' | 'free';\n  curl: number;\n  amp: number;\n  freq: number;\n  wave: number;\n  rA: number;\n  rB: number;\n  speed: number;\n  splay: number;\n  core: number;\n  rest: number;\n  alpha: number;\n  th: number;\n  ax: number;\n  ay: number;\n  vx: number;\n  vy: number;\n  hang: number;\n  pts: number[];\n}\n\ninterface Shift {\n  el: HTMLLIElement;\n  y: number;\n  v: number;\n  target: number;\n  delay: number;\n  item: HTMLDivElement | null;\n}\n\ninterface Sim<T> {\n  raf: number;\n  last: number;\n  t: number;\n  drag: Drag<T> | null;\n  feeds: Feed<T>[];\n  strips: Strip<T>[];\n  shifts: Map<HTMLLIElement, Shift>;\n  tops: Map<HTMLLIElement, number>;\n  timers: Set<ReturnType<typeof setTimeout>>;\n  dpr: number;\n  cw: number;\n  ch: number;\n}\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\nconst ease = (from: number, to: number, dt: number, tau: number) => from + (to - from) * (1 - Math.exp(-dt / tau));\nconst smooth = (t: number) => {\n  const u = clamp(t, 0, 1);\n  return u * u * (3 - 2 * u);\n};\nconst pick = (lo: number, hi: number) => lo + Math.random() * (hi - lo);\nconst side = () => (Math.random() < 0.5 ? -1 : 1);\nconst reduced = () =>\n  typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n\nconst spring = (p: number, v: number, target: number, dt: number, omega: number): [number, number] => {\n  const offset = p - target;\n  const term = v + omega * offset;\n  const decay = Math.exp(-omega * dt);\n  return [target + (offset + term * dt) * decay, (v - omega * term * dt) * decay];\n};\n\nconst urls = new Map<string, Promise<string>>();\n\nconst dataUrl = (src: string): Promise<string> => {\n  if (!src || src.startsWith('data:')) return Promise.resolve(src);\n  const hit = urls.get(src);\n  if (hit) return hit;\n  const job = fetch(src, { mode: 'cors' })\n    .then(res => res.blob())\n    .then(\n      blob =>\n        new Promise<string>((resolve, reject) => {\n          const reader = new FileReader();\n          reader.onload = () => resolve(reader.result as string);\n          reader.onerror = reject;\n          reader.readAsDataURL(blob);\n        })\n    );\n  urls.set(src, job);\n  job.catch(() => urls.delete(src));\n  return job;\n};\n\nconst snapshot = (node: HTMLElement, W: number, H: number): Promise<Texture> => {\n  const clone = node.cloneNode(true) as HTMLElement;\n  const src = [node, ...node.querySelectorAll<HTMLElement>('*')];\n  const dst = [clone, ...clone.querySelectorAll<HTMLElement>('*')];\n  const images: [HTMLImageElement, string][] = [];\n  for (let i = 0; i < src.length; i += 1) {\n    const cs = getComputedStyle(src[i]);\n    const style = dst[i].style;\n    for (let j = 0; j < cs.length; j += 1) style.setProperty(cs[j], cs.getPropertyValue(cs[j]));\n    const from = src[i];\n    const to = dst[i];\n    if (to instanceof HTMLImageElement && from instanceof HTMLImageElement)\n      images.push([to, from.currentSrc || from.src]);\n  }\n  Object.assign(clone.style, {\n    position: 'relative',\n    inset: 'auto',\n    margin: '0',\n    transform: 'none',\n    filter: 'none',\n    transition: 'none',\n    width: `${W}px`,\n    height: `${H}px`\n  });\n  clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');\n  const scale = Math.min(3, window.devicePixelRatio || 1);\n  return Promise.all(\n    images.map(async ([img, from]) => {\n      img.removeAttribute('srcset');\n      img.removeAttribute('loading');\n      try {\n        img.setAttribute('src', await dataUrl(from));\n      } catch {\n        img.removeAttribute('src');\n        img.style.background = 'rgba(127, 127, 127, 0.25)';\n      }\n    })\n  ).then(\n    () =>\n      new Promise<Texture>((resolve, reject) => {\n        const markup = new XMLSerializer().serializeToString(clone);\n        const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${W}\" height=\"${H}\"><foreignObject width=\"${W}\" height=\"${H}\">${markup}</foreignObject></svg>`;\n        const img = new Image();\n        img.onload = () => {\n          const tex = document.createElement('canvas');\n          tex.width = Math.ceil(W * scale);\n          tex.height = Math.ceil(H * scale);\n          tex.getContext('2d')!.drawImage(img, 0, 0, tex.width, tex.height);\n          resolve({ tex, scale });\n        };\n        img.onerror = () => reject(new Error('snapshot'));\n        img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n      })\n  );\n};\n\nconst flat = (W: number, H: number, fill: string): Texture => {\n  const tex = document.createElement('canvas');\n  tex.width = W;\n  tex.height = H;\n  const ctx = tex.getContext('2d')!;\n  ctx.fillStyle = fill;\n  ctx.fillRect(0, 0, W, H);\n  return { tex, scale: 1 };\n};\n\nconst shape = <T,>(st: Strip<T>, len: number, time: number, tear: number) => {\n  const pts = st.pts;\n  pts.length = 0;\n  let x = st.ax;\n  let y = st.ay;\n  const steps = Math.ceil(len / SLIVER);\n  const swing = (3 + 9 * st.rA) * tear;\n  const pace = time * (1.6 + st.rA * 2.2) + st.rA * 6.2832;\n  for (let j = 0; j <= steps; j += 1) {\n    const d = Math.min(j * SLIVER, len);\n    const a = st.th + st.curl * d + st.amp * Math.sin(d * st.freq + st.wave);\n    const flutter = swing * Math.sin(d * 0.045 + pace);\n    pts.push(x + flutter * Math.cos(a), y - flutter * Math.sin(a), a);\n    const h = Math.min(SLIVER, len - d);\n    x += Math.sin(a) * h;\n    y += Math.cos(a) * h;\n  }\n};\n\nconst middle = (pts: number[]) => {\n  let y = 0;\n  for (let j = 1; j < pts.length; j += 3) y += pts[j];\n  return (y * 3) / pts.length;\n};\n\nconst paintStrip = <T,>(ctx: CanvasRenderingContext2D, st: Strip<T>, len: number, dpr: number) => {\n  const { tex, ts } = st.feed;\n  if (!tex) return;\n  const pts = st.pts;\n  const wc = st.w * st.core;\n  const half = wc / 2;\n  const sx = (st.x + (st.w - wc) / 2) * ts;\n  const v0 = st.H - len;\n  ctx.globalAlpha = st.alpha;\n  for (let j = 0; j + 3 < pts.length; j += 3) {\n    const d = (j / 3) * SLIVER;\n    const h = Math.min(SLIVER, len - d);\n    if (h <= 0) break;\n    const ca = Math.cos(pts[j + 2]);\n    const sa = Math.sin(pts[j + 2]);\n    ctx.setTransform(dpr * ca, -dpr * sa, dpr * sa, dpr * ca, dpr * (pts[j] + OVER), dpr * pts[j + 1]);\n    ctx.drawImage(tex, sx, (v0 + d) * ts, wc * ts, h * ts, -half, 0, wc, h + 0.4);\n  }\n  ctx.globalAlpha = 1;\n};\n\nconst Shredder = <T extends ShredderItem>({\n  items = [],\n  renderItem,\n  onShred,\n  onReorder,\n  width = 340,\n  height = 460,\n  inset = 14,\n  gap = 10,\n  slitHeight = 4,\n  fallHeight = 140,\n  feedSpeed = 180,\n  bite = 18,\n  autoFeed = true,\n  stripWidth = 10,\n  curl = 1,\n  autoAnimate = false,\n  loop = false,\n  loopAfterDelete = false,\n  dragTilt = 6,\n  lift = 1.02,\n  slitColor = '#3f3f46',\n  color = '#f5f5f5',\n  disabled = false,\n  className = ''\n}: ShredderProps<T>) => {\n  const [order, setOrder] = useState(() => items.map(item => item.id));\n  const rank = new Map<string | number, number>(order.map((id, i) => [id, i]));\n  const weight = (item: T) => rank.get(item.id) ?? order.length + items.indexOf(item);\n  const sorted = [...items].sort((a, b) => weight(a) - weight(b));\n  const rootRef = useRef<HTMLDivElement>(null);\n  const slitRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const slotEls = useRef(new Map<string | number, HTMLLIElement>());\n  const itemEls = useRef(new Map<string | number, HTMLDivElement>());\n  const cfg = useRef<Config<T>>({} as Config<T>);\n  cfg.current = {\n    items: sorted,\n    height,\n    inset,\n    gap,\n    slitHeight,\n    fallHeight,\n    feedSpeed,\n    bite,\n    autoFeed,\n    stripWidth,\n    curl,\n    loop,\n    loopAfterDelete,\n    dragTilt,\n    lift,\n    onShred,\n    onReorder,\n    disabled\n  };\n  const sim = useRef<Sim<T>>({\n    raf: 0,\n    last: 0,\n    t: 0,\n    drag: null,\n    feeds: [],\n    strips: [],\n    shifts: new Map(),\n    tops: new Map(),\n    timers: new Set(),\n    dpr: 1,\n    cw: 0,\n    ch: 0\n  });\n\n  const metrics = (): Metrics => {\n    const c = cfg.current;\n    const root = rootRef.current!;\n    const r = root.getBoundingClientRect();\n    const k = r.width / root.offsetWidth || 1;\n    const lip = c.height - c.fallHeight - c.slitHeight;\n    return { left: r.left, top: r.top, k, rw: root.offsetWidth, lip, exit: lip + c.slitHeight };\n  };\n  const at = (el: Element, m: Metrics) => {\n    const r = el.getBoundingClientRect();\n    return { x: (r.left - m.left) / m.k, y: (r.top - m.top) / m.k };\n  };\n  const local = (e: { clientX: number; clientY: number }, m: Metrics) => ({\n    x: (e.clientX - m.left) / m.k,\n    y: (e.clientY - m.top) / m.k\n  });\n\n  const run = () => {\n    const s = sim.current;\n    if (s.raf) return;\n    s.last = performance.now();\n    s.raf = requestAnimationFrame(step);\n  };\n\n  const later = (ms: number, fn: () => void) => {\n    const s = sim.current;\n    const id = setTimeout(() => {\n      s.timers.delete(id);\n      fn();\n    }, ms);\n    s.timers.add(id);\n  };\n\n  const slotOf = (key: string | number) => slotEls.current.get(key);\n  const isLive = (key: string | number) => {\n    const slot = slotOf(key);\n    return !!slot && slot.dataset.gone === undefined;\n  };\n  const liveKeys = () => cfg.current.items.map(item => item.id).filter(isLive);\n  const goneKeys = () =>\n    cfg.current.items\n      .map(item => item.id)\n      .filter(key => {\n        const slot = slotOf(key);\n        return !!slot && slot.dataset.gone !== undefined;\n      });\n\n  const shiftOf = (slot: HTMLLIElement): Shift => {\n    const s = sim.current;\n    let sh = s.shifts.get(slot);\n    if (!sh) {\n      sh = { el: slot, y: 0, v: 0, target: 0, delay: 0, item: null };\n      s.shifts.set(slot, sh);\n    }\n    return sh;\n  };\n\n  const settle = (origin: HTMLLIElement | null, entering: Set<HTMLLIElement> | null) => {\n    const s = sim.current;\n    if (!rootRef.current) return;\n    const m = metrics();\n    const next = new Map<HTMLLIElement, number>();\n    const still = reduced();\n    const active = s.drag ? s.drag.slot : null;\n    const slots = cfg.current.items.map(item => slotOf(item.id));\n    const from = origin ? slots.indexOf(origin) : -1;\n    let born = 0;\n    cfg.current.items.forEach((item, idx) => {\n      const slot = slots[idx];\n      if (!slot) return;\n      const sh = s.shifts.get(slot);\n      const top = at(slot, m).y - (sh ? sh.y : 0);\n      const prev = s.tops.get(slot);\n      next.set(slot, top);\n      if (slot === active || still) return;\n      const fresh = (entering && entering.has(slot)) || (prev === undefined && s.tops.size > 0);\n      if (fresh) {\n        const rec = shiftOf(slot);\n        const el = itemEls.current.get(item.id) || null;\n        rec.y = -ENTER;\n        rec.v = 0;\n        rec.target = 0;\n        rec.delay = born * 0.05;\n        rec.item = el;\n        born += 1;\n        if (el) el.style.opacity = '0';\n        slot.style.transform = `translateY(${rec.y}px)`;\n        return;\n      }\n      if (prev === undefined) return;\n      const delta = prev - top;\n      if (Math.abs(delta) < 0.5) return;\n      const rec = shiftOf(slot);\n      rec.y += delta;\n      if (from >= 0) rec.delay = Math.max(0, from - 1 - idx) * STAGGER;\n      slot.style.transform = `translateY(${rec.y}px)`;\n    });\n    s.tops = next;\n    run();\n  };\n\n  const reflow = (mutate: () => void, origin?: HTMLLIElement | null, entering?: Set<HTMLLIElement> | null) => {\n    mutate();\n    settle(origin || null, entering || null);\n  };\n\n  const arrange = (key: string | number, index: number) => {\n    const ids = cfg.current.items.map(item => item.id).filter(id => id !== key);\n    const live = ids.filter(isLive);\n    let anchor = 0;\n    if (index < live.length) anchor = ids.indexOf(live[index]);\n    else if (live.length) anchor = ids.indexOf(live[live.length - 1]) + 1;\n    ids.splice(anchor, 0, key);\n    return ids;\n  };\n\n  const commit = (ids: (string | number)[]) => {\n    const same = ids.every((id, i) => id === cfg.current.items[i]?.id);\n    if (same) return false;\n    flushSync(() => setOrder(ids));\n    return true;\n  };\n\n  const revive = (keys: (string | number)[], index?: number) => {\n    const s = sim.current;\n    if (s.drag) {\n      later(300, () => revive(keys, index));\n      return;\n    }\n    const entering = new Set<HTMLLIElement>();\n    reflow(\n      () => {\n        if (index !== undefined && keys.length === 1) commit(arrange(keys[0], index));\n        keys.forEach(key => {\n          const slot = slotOf(key);\n          const el = itemEls.current.get(key);\n          if (!slot || slot.dataset.gone === undefined) return;\n          delete slot.dataset.gone;\n          slot.style.height = '';\n          slot.style.marginBottom = '';\n          if (el) {\n            el.style.visibility = '';\n            el.style.transform = '';\n            delete el.dataset.state;\n          }\n          entering.add(slot);\n        });\n      },\n      null,\n      entering\n    );\n  };\n\n  const afterShred = (key: string | number) => {\n    const c = cfg.current;\n    if (c.loopAfterDelete) {\n      later(700, () => revive([key], Math.floor(Math.random() * (liveKeys().length + 1))));\n    } else if (c.loop && liveKeys().length === 0) {\n      later(900, () => revive(goneKeys()));\n    }\n  };\n\n  const collapse = (f: Feed<T>, s: Sim<T>) => {\n    f.consumed = true;\n    f.el.style.visibility = 'hidden';\n    delete f.slot.dataset.active;\n    s.shifts.forEach(sh => {\n      sh.target = 0;\n    });\n    reflow(() => {\n      f.slot.dataset.gone = '';\n      f.slot.style.height = '0px';\n      f.slot.style.marginBottom = '0px';\n    }, f.slot);\n    cfg.current.onShred?.(f.item);\n    afterShred(f.key);\n  };\n\n  const consumeNow = (key: string | number, item: T, el: HTMLDivElement, slot: HTMLLIElement) => {\n    const s = sim.current;\n    const f: Feed<T> = {\n      key,\n      item,\n      el,\n      slot,\n      W: 0,\n      H: 0,\n      rx: 0,\n      ry: 0,\n      tx: 0,\n      tilt: 0,\n      lift: 1,\n      v: 0,\n      age: 0,\n      tex: null,\n      ts: 1,\n      consumed: false,\n      strips: 0\n    };\n    collapse(f, s);\n    run();\n  };\n\n  const grab = (d: Drag<T>, m: Metrics) => {\n    const s = sim.current;\n    const c = cfg.current;\n    s.drag = null;\n    s.shifts.forEach(sh => {\n      sh.target = 0;\n    });\n    if (reduced()) {\n      consumeNow(d.key, d.item, d.el, d.slot);\n      return;\n    }\n    const over = d.ry + d.H - m.lip;\n    const ry = over > c.bite * 1.5 ? m.lip + c.bite * 1.5 - d.H : d.ry;\n    const lo = c.inset - SLIT;\n    const hi = m.rw - c.inset + SLIT - d.W;\n    const n = Math.max(1, Math.round(d.W / Math.max(4, c.stripWidth)));\n    const sw = Math.floor(d.W / n);\n    const f: Feed<T> = {\n      key: d.key,\n      item: d.item,\n      el: d.el,\n      slot: d.slot,\n      W: d.W,\n      H: d.H,\n      rx: d.rx,\n      ry,\n      tx: clamp(d.rx, Math.min(lo, hi), Math.max(lo, hi)),\n      tilt: d.tilt,\n      lift: d.lift,\n      v: c.feedSpeed * TUG,\n      age: 0,\n      tex: null,\n      ts: 1,\n      consumed: false,\n      strips: n\n    };\n    let settled = false;\n    const use = ({ tex, scale }: Texture) => {\n      if (settled) return;\n      settled = true;\n      f.tex = tex;\n      f.ts = scale;\n      run();\n    };\n    d.snap.then(use, () => use(flat(d.W, d.H, d.fill)));\n    setTimeout(() => use(flat(d.W, d.H, d.fill)), 400);\n    d.el.dataset.state = 'feed';\n    s.feeds.push(f);\n    for (let i = 0; i < n; i += 1) {\n      s.strips.push({\n        feed: f,\n        x: i * sw,\n        w: i === n - 1 ? d.W - sw * (n - 1) : sw,\n        H: d.H,\n        phase: 'attached',\n        curl: side() * pick(0.35, 0.9) * DEG * c.curl,\n        amp: pick(1.5, 4) * DEG * c.curl,\n        freq: (Math.PI * 2) / pick(34, 60),\n        wave: pick(0, Math.PI * 2),\n        rA: Math.random(),\n        rB: Math.random(),\n        speed: 0,\n        splay: 0,\n        core: 1,\n        rest: 0,\n        alpha: 1,\n        th: 0,\n        ax: 0,\n        ay: 0,\n        vx: 0,\n        vy: 0,\n        hang: 0,\n        pts: []\n      });\n    }\n  };\n\n  const place = (d: Drag<T>, sp: { x: number; y: number }) => {\n    d.el.style.transform = `translate(${d.rx - sp.x}px, ${d.ry - sp.y}px) rotate(${d.tilt}deg) scale(${d.lift})`;\n  };\n\n  const aim = (d: Drag<T>, m: Metrics) => {\n    const s = sim.current;\n    const c = cfg.current;\n    const rows: { slot: HTMLLIElement; mid: number }[] = [];\n    c.items.forEach(item => {\n      if (item.id === d.key) return;\n      const slot = slotOf(item.id);\n      if (!slot || slot.dataset.gone !== undefined) return;\n      const top = s.tops.get(slot);\n      if (top === undefined) return;\n      rows.push({ slot, mid: top + slot.offsetHeight / 2 });\n    });\n    let j = -1;\n    if (d.moved && d.ry + d.H < m.lip - 8) {\n      const cy = d.ry + d.H / 2;\n      let n = 0;\n      rows.forEach(row => {\n        if (row.mid < cy) n += 1;\n      });\n      if (d.j >= 0 && n !== d.j) {\n        const edge = rows[n > d.j ? n - 1 : n];\n        if (edge && Math.abs(edge.mid - cy) < HYSTERESIS) n = d.j;\n      }\n      j = n;\n    }\n    if (j === d.j) return;\n    d.j = j;\n    rows.forEach((row, k) => {\n      shiftOf(row.slot).target = j >= 0 && k < j ? -(d.H + c.gap) : 0;\n    });\n  };\n\n  const drop = (d: Drag<T>) => {\n    const s = sim.current;\n    const c = cfg.current;\n    const live = liveKeys().filter(key => key !== d.key);\n    const index = clamp(d.j >= 0 ? d.j : d.from, 0, live.length);\n    s.shifts.forEach(sh => {\n      sh.target = 0;\n    });\n    let changed = false;\n    reflow(() => {\n      changed = commit(arrange(d.key, index));\n      d.slot.style.height = `${d.H}px`;\n      d.slot.style.marginBottom = '';\n    });\n    d.phase = 'return';\n    if (changed) c.onReorder?.(cfg.current.items);\n  };\n\n  const tick = (now: number) => {\n    const s = sim.current;\n    const c = cfg.current;\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    if (!root || !canvas) {\n      s.raf = 0;\n      return;\n    }\n    const dt = clamp((now - s.last) / 1000, 0, 0.05);\n    s.last = now;\n    s.t += dt;\n    const m = metrics();\n    const d = s.drag;\n    if (d) {\n      if (!d.el.isConnected) {\n        s.drag = null;\n      } else {\n        const sp = at(d.slot, m);\n        const ceiling = m.lip + c.bite - d.H;\n        if (d.phase === 'drag') {\n          d.tx = d.px - d.gx;\n          d.ty = Math.min(d.py - d.gy, ceiling);\n          if (!d.moved && Math.hypot(d.px - d.ox, d.py - d.oy) > 6) d.moved = true;\n          aim(d, m);\n        } else if (d.phase === 'carry') {\n          d.tx = (m.rw - d.W) / 2;\n          d.ty = ceiling;\n        } else {\n          d.tx = sp.x;\n          d.ty = sp.y;\n        }\n        const omega = d.phase === 'drag' ? FOLLOW : SETTLE;\n        [d.rx, d.vx] = spring(d.rx, d.vx, d.tx, dt, omega);\n        [d.ry, d.vy] = spring(d.ry, d.vy, d.ty, dt, omega);\n        const lean = d.phase === 'drag' ? clamp(d.vx * 0.012, -c.dragTilt, c.dragTilt) : 0;\n        d.tilt = ease(d.tilt, lean, dt, 0.09);\n        d.lift = ease(d.lift, d.phase === 'return' ? 1 : c.lift, dt, 0.12);\n        const over = d.ry + d.H - m.lip;\n        const bites = d.phase === 'carry' || (d.phase === 'drag' && c.autoFeed);\n        if (bites && over >= c.bite - 0.5) {\n          grab(d, m);\n        } else {\n          place(d, sp);\n          const still =\n            Math.abs(d.rx - d.tx) < 0.2 && Math.abs(d.ry - d.ty) < 0.2 && Math.abs(d.vy) < 2 && Math.abs(d.vx) < 2;\n          if (d.phase === 'return' && still && Math.abs(d.lift - 1) < 0.002 && Math.abs(d.tilt) < 0.05) {\n            d.el.style.transform = '';\n            delete d.el.dataset.state;\n            delete d.slot.dataset.active;\n            d.slot.style.height = '';\n            s.drag = null;\n          }\n        }\n      }\n    }\n    let pulling = false;\n    for (let i = s.feeds.length - 1; i >= 0; i -= 1) {\n      const f = s.feeds[i];\n      if (!f.consumed) {\n        if (!f.el.isConnected) {\n          s.feeds.splice(i, 1);\n          continue;\n        }\n        pulling = true;\n        const sp = at(f.slot, m);\n        if (f.tex) {\n          f.age += dt;\n          f.v = c.feedSpeed * (1 + (TUG - 1) * Math.exp(-f.age / TUG_DECAY));\n          f.ry += f.v * dt;\n        }\n        f.rx = ease(f.rx, f.tx, dt, 0.1);\n        f.tilt = ease(f.tilt, 0, dt, 0.08);\n        f.lift = ease(f.lift, 1, dt, 0.1);\n        if (f.ry >= m.lip) {\n          collapse(f, s);\n        } else {\n          const jitter = Math.sin(s.t * 150) * 0.5;\n          f.el.style.transform = `translate(${f.rx - sp.x + jitter}px, ${f.ry - sp.y}px) rotate(${f.tilt}deg) scale(${f.lift})`;\n        }\n      } else {\n        f.ry += f.v * dt;\n        if (f.strips === 0) s.feeds.splice(i, 1);\n      }\n    }\n    let moving = false;\n    s.shifts.forEach((sh, el) => {\n      if (!el.isConnected) {\n        s.shifts.delete(el);\n        return;\n      }\n      if (sh.delay > 0) {\n        sh.delay -= dt;\n        moving = true;\n        return;\n      }\n      if (sh.item) {\n        const el = sh.item;\n        el.style.transition = 'opacity 280ms ease';\n        el.style.opacity = '';\n        setTimeout(() => {\n          el.style.transition = '';\n        }, 320);\n        sh.item = null;\n      }\n      const n = Math.ceil(dt * 240);\n      const h = dt / n;\n      for (let k = 0; k < n; k += 1) {\n        sh.v += (-STACK_K * (sh.y - sh.target) - STACK_C * sh.v) * h;\n        sh.y += sh.v * h;\n      }\n      if (Math.abs(sh.y - sh.target) < 0.15 && Math.abs(sh.v) < 4) {\n        sh.y = sh.target;\n        sh.v = 0;\n        if (sh.target === 0) {\n          el.style.transform = '';\n          s.shifts.delete(el);\n        } else {\n          el.style.transform = `translateY(${sh.y}px)`;\n        }\n      } else {\n        el.style.transform = `translateY(${sh.y}px)`;\n        moving = true;\n      }\n    });\n    const ctx = canvas.getContext('2d')!;\n    const dpr = s.dpr;\n    if (s.strips.length) {\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n    }\n    for (let i = s.strips.length - 1; i >= 0; i -= 1) {\n      const st = s.strips[i];\n      const f = st.feed;\n      let len = st.H;\n      if (st.phase === 'attached') {\n        const top = f.ry - m.exit;\n        st.hang = top + st.H;\n        if (st.hang <= 0) {\n          moving = true;\n          continue;\n        }\n        st.ax = f.rx + st.x + st.w / 2;\n        st.th = Math.sin(s.t * 6 + st.wave) * 0.03 * smooth(st.hang / 40);\n        if (top >= 0) {\n          st.phase = 'free';\n          st.ay = top;\n          st.vy = f.v;\n          st.vx = pick(-20, 20);\n          st.speed = 150 + st.rA * 230;\n          st.splay = (st.rA - 0.5) * 320;\n          st.rest = side() * pick(0.15, 0.6);\n          f.strips -= 1;\n        } else {\n          st.ay = 0;\n          len = st.hang;\n        }\n      }\n      if (st.phase === 'free') {\n        const tear = Math.pow(clamp(middle(st.pts) / c.fallHeight, 0, 1), 1.2);\n        const breath = 0.85 + 0.15 * Math.sin(s.t * (1 + st.rB * 2) + st.rA * 6.2832);\n        st.vy = ease(st.vy, st.speed * breath, dt, 0.25);\n        st.vx = ease(st.vx, st.splay * tear + (st.ax - m.rw / 2) * 0.25, dt, 0.4);\n        st.ax += st.vx * dt;\n        st.ay += st.vy * dt;\n        st.th = ease(st.th, st.rest, dt, 0.5);\n        st.core = 1 - smooth(tear / 0.85) * (0.8 - st.rA * 0.16);\n        shape(st, st.H, s.t, tear);\n        st.alpha = 1 - smooth((tear - 0.6) / 0.4);\n        if (st.alpha <= 0.01 || st.pts[1] > c.fallHeight) {\n          s.strips.splice(i, 1);\n          continue;\n        }\n      } else {\n        st.core = 1;\n        shape(st, len, s.t, 0);\n        st.alpha = 1;\n      }\n      paintStrip(ctx, st, len, dpr);\n      moving = true;\n    }\n    const slit = slitRef.current;\n    if (slit) {\n      slit.style.transform = pulling ? `translate(${Math.sin(s.t * 140) * 0.5}px, ${Math.cos(s.t * 97) * 0.35}px)` : '';\n    }\n    if (s.drag || s.feeds.length || moving) {\n      s.raf = requestAnimationFrame(step);\n    } else {\n      s.raf = 0;\n    }\n  };\n\n  const step = (now: number) => {\n    const s = sim.current;\n    try {\n      tick(now);\n    } catch (err) {\n      s.raf = 0;\n      throw err;\n    }\n  };\n\n  const begin = (item: T, phase: Drag<T>['phase'], e: PointerEvent<HTMLDivElement> | null) => {\n    const s = sim.current;\n    const c = cfg.current;\n    const key = item.id;\n    const el = itemEls.current.get(key);\n    const slot = slotOf(key);\n    if (c.disabled || s.drag || !el || !slot || slot.dataset.gone !== undefined) return;\n    if (s.feeds.some(f => f.key === key)) return;\n    const m = metrics();\n    const sp = at(slot, m);\n    const W = slot.offsetWidth;\n    const H = slot.offsetHeight;\n    const p = e ? local(e, m) : { x: sp.x, y: sp.y };\n    const face = (el.firstElementChild as HTMLElement | null) || el;\n    const fill = getComputedStyle(face).backgroundColor;\n    let snap: Promise<Texture>;\n    try {\n      snap = snapshot(el, W, H);\n    } catch {\n      snap = Promise.reject(new Error('snapshot'));\n    }\n    snap.catch(() => {});\n    const d: Drag<T> = {\n      key,\n      item,\n      el,\n      slot,\n      W,\n      H,\n      rx: sp.x,\n      ry: sp.y,\n      tx: sp.x,\n      ty: sp.y,\n      vx: 0,\n      vy: 0,\n      tilt: 0,\n      lift: 1,\n      gx: p.x - sp.x,\n      gy: p.y - sp.y,\n      px: p.x,\n      py: p.y,\n      ox: p.x,\n      oy: p.y,\n      moved: false,\n      from: liveKeys().indexOf(key),\n      j: -1,\n      phase,\n      id: e ? e.pointerId : null,\n      snap,\n      fill: fill === 'rgba(0, 0, 0, 0)' || fill === 'transparent' ? 'rgba(127, 127, 127, 0.35)' : fill\n    };\n    s.drag = d;\n    s.shifts.delete(slot);\n    slot.style.transform = '';\n    reflow(() => {\n      slot.dataset.active = '';\n      el.dataset.state = 'drag';\n      slot.style.height = '0px';\n      slot.style.marginBottom = '0px';\n    }, slot);\n    place(d, at(slot, m));\n    run();\n  };\n\n  const onDown = (e: PointerEvent<HTMLDivElement>, item: T) => {\n    if (e.button !== 0) return;\n    try {\n      e.currentTarget.setPointerCapture(e.pointerId);\n    } catch {}\n    begin(item, 'drag', e);\n  };\n  const onMove = (e: PointerEvent<HTMLDivElement>) => {\n    const d = sim.current.drag;\n    if (!d || d.id !== e.pointerId) return;\n    const p = local(e, metrics());\n    d.px = p.x;\n    d.py = p.y;\n  };\n  const onUp = (e: PointerEvent<HTMLDivElement>) => {\n    const d = sim.current.drag;\n    if (!d || d.id !== e.pointerId) return;\n    d.id = null;\n    try {\n      if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);\n    } catch {}\n    const m = metrics();\n    if (d.ry + d.H > m.lip + 0.5) {\n      grab(d, m);\n    } else {\n      drop(d);\n    }\n    run();\n  };\n  const onKey = (e: KeyboardEvent<HTMLDivElement>, item: T) => {\n    if (e.key !== 'Delete' && e.key !== 'Backspace') return;\n    e.preventDefault();\n    if (e.repeat) return;\n    const key = item.id;\n    if (reduced()) {\n      const el = itemEls.current.get(key);\n      const slot = slotOf(key);\n      const s = sim.current;\n      if (cfg.current.disabled || !el || !slot || slot.dataset.gone !== undefined || s.feeds.some(f => f.key === key))\n        return;\n      consumeNow(key, item, el, slot);\n      return;\n    }\n    begin(item, 'carry', null);\n  };\n\n  useLayoutEffect(() => {\n    settle(null, null);\n  });\n\n  useEffect(() => {\n    const s = sim.current;\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    if (!root || !canvas) return undefined;\n    const fit = () => {\n      const dpr = Math.min(3, window.devicePixelRatio || 1);\n      const cw = root.offsetWidth + OVER * 2;\n      const ch = cfg.current.fallHeight;\n      if (cw === s.cw && ch === s.ch && dpr === s.dpr) return;\n      if (s.cw) s.tops = new Map();\n      s.cw = cw;\n      s.ch = ch;\n      s.dpr = dpr;\n      canvas.width = Math.ceil(cw * dpr);\n      canvas.height = Math.ceil(ch * dpr);\n    };\n    const ro = new ResizeObserver(fit);\n    ro.observe(root);\n    fit();\n    return () => ro.disconnect();\n  }, [fallHeight]);\n\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      itemEls.current.forEach(el => {\n        el.querySelectorAll('img').forEach(img => {\n          dataUrl(img.currentSrc || img.src).catch(() => {});\n        });\n      });\n    }, 300);\n    return () => clearTimeout(timer);\n  }, [items]);\n\n  useEffect(() => {\n    if (!autoAnimate) return undefined;\n    const s = sim.current;\n    let alive = true;\n    let timer: ReturnType<typeof setTimeout> | undefined;\n    const wait = (ms: number) =>\n      new Promise(resolve => {\n        timer = setTimeout(resolve, ms);\n      });\n    const idle = () => !s.drag && s.feeds.length === 0 && s.strips.length === 0 && s.shifts.size === 0;\n    const pickNext = () => {\n      const list = cfg.current.items;\n      for (let i = list.length - 1; i >= 0; i -= 1) {\n        if (isLive(list[i].id)) return list[i];\n      }\n      return null;\n    };\n    const play = async () => {\n      await wait(900);\n      while (alive) {\n        while (alive && !idle()) await wait(120);\n        if (!alive) break;\n        const item = pickNext();\n        if (!item) {\n          await wait(1100);\n          if (!alive) break;\n          revive(goneKeys());\n          await wait(1200);\n          continue;\n        }\n        if (!cfg.current.disabled) begin(item, 'carry', null);\n        await wait(700);\n      }\n    };\n    play();\n    return () => {\n      alive = false;\n      clearTimeout(timer);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [autoAnimate]);\n\n  useEffect(() => {\n    const s = sim.current;\n    return () => {\n      cancelAnimationFrame(s.raf);\n      s.raf = 0;\n      s.timers.forEach(id => clearTimeout(id));\n      s.timers.clear();\n    };\n  }, []);\n\n  const keep =\n    <E extends HTMLElement>(map: { current: Map<string | number, E> }, key: string | number) =>\n    (el: E | null) => {\n      if (el) map.current.set(key, el);\n      else map.current.delete(key);\n    };\n\n  return (\n    <div\n      ref={rootRef}\n      className={`group relative [width:min(var(--sh-w),100%)] [height:var(--sh-h)] [color:var(--sh-ink)] [-webkit-tap-highlight-color:transparent] [-webkit-touch-callout:none] data-[disabled]:opacity-60${className ? ` ${className}` : ''}`}\n      data-disabled={disabled ? '' : undefined}\n      style={\n        {\n          '--sh-w': `${width}px`,\n          '--sh-h': `${height}px`,\n          '--sh-inset': `${inset}px`,\n          '--sh-gap': `${gap}px`,\n          '--sh-slit-h': `${slitHeight}px`,\n          '--sh-fall': `${fallHeight}px`,\n          '--sh-slit-inset': `${Math.max(0, inset - SLIT)}px`,\n          '--sh-over': `${OVER}px`,\n          '--sh-slit': slitColor,\n          '--sh-ink': color\n        } as CSSProperties\n      }\n    >\n      <ul className=\"absolute right-0 left-0 z-[1] m-0 list-none [bottom:calc(var(--sh-fall)+var(--sh-slit-h))] [padding:0_var(--sh-inset)] [clip-path:inset(-9999px_-9999px_0_-9999px)]\">\n        {sorted.map((item, index) => (\n          <li\n            key={item.id}\n            ref={keep(slotEls, item.id)}\n            className=\"group/slot relative [margin-bottom:var(--sh-gap)] data-[active]:z-[2] data-[gone]:pointer-events-none\"\n          >\n            <div\n              ref={keep(itemEls, item.id)}\n              className=\"relative origin-center cursor-grab touch-none select-none outline-none [-webkit-user-select:none] group-data-[active]/slot:absolute group-data-[active]/slot:top-0 group-data-[active]/slot:right-0 group-data-[active]/slot:left-0 group-data-[active]/slot:[will-change:transform,filter] group-data-[active]/slot:[filter:drop-shadow(0_0_0_rgba(0,0,0,0))] group-data-[active]/slot:[transition:filter_240ms_ease] data-[state=drag]:cursor-grabbing data-[state=drag]:[filter:drop-shadow(0_14px_22px_rgba(0,0,0,0.22))]! data-[state=feed]:pointer-events-none data-[state=feed]:cursor-default group-data-[disabled]:cursor-default motion-reduce:group-data-[active]/slot:[transition:none]\"\n              tabIndex={disabled ? -1 : 0}\n              aria-disabled={disabled || undefined}\n              onPointerDown={e => onDown(e, item)}\n              onPointerMove={onMove}\n              onPointerUp={onUp}\n              onPointerCancel={onUp}\n              onLostPointerCapture={onUp}\n              onKeyDown={e => onKey(e, item)}\n              onDragStart={e => e.preventDefault()}\n            >\n              {renderItem(item, index)}\n            </div>\n          </li>\n        ))}\n      </ul>\n      <div\n        ref={slitRef}\n        className=\"absolute z-[3] rounded-full will-change-transform [right:var(--sh-slit-inset)] [bottom:var(--sh-fall)] [left:var(--sh-slit-inset)] [height:var(--sh-slit-h)] [background:var(--sh-slit)]\"\n        aria-hidden=\"true\"\n      />\n      <div\n        className=\"pointer-events-none absolute right-0 bottom-0 left-0 z-[2] [height:var(--sh-fall)]\"\n        aria-hidden=\"true\"\n      >\n        <canvas\n          ref={canvasRef}\n          className=\"absolute top-0 block h-full [left:calc(-1*var(--sh-over))] [width:calc(100%+var(--sh-over)*2)]\"\n        />\n      </div>\n    </div>\n  );\n};\n\nexport default Shredder;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"react-dom@^19.0.0"
	]
}