{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "Shredder-TS-CSS",
	"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:file",
			"path": "Shredder.css",
			"target": "@components/Shredder.css",
			"content": ".shredder {\n  position: relative;\n  width: min(var(--sh-w), 100%);\n  height: var(--sh-h);\n  color: var(--sh-ink);\n  -webkit-tap-highlight-color: transparent;\n  -webkit-touch-callout: none;\n}\n\n.shredder[data-disabled] {\n  opacity: 0.6;\n}\n\n.shredder__list {\n  position: absolute;\n  right: 0;\n  bottom: calc(var(--sh-fall) + var(--sh-slit-h));\n  left: 0;\n  z-index: 1;\n  margin: 0;\n  padding: 0 var(--sh-inset);\n  list-style: none;\n  clip-path: inset(-9999px -9999px 0 -9999px);\n}\n\n.shredder__slot {\n  position: relative;\n  margin-bottom: var(--sh-gap);\n}\n\n.shredder__slot[data-active] {\n  z-index: 2;\n}\n\n.shredder__slot[data-gone] {\n  pointer-events: none;\n}\n\n.shredder__item {\n  position: relative;\n  outline: none;\n  cursor: grab;\n  touch-action: none;\n  user-select: none;\n  -webkit-user-select: none;\n  transform-origin: 50% 50%;\n}\n\n.shredder__slot[data-active] .shredder__item {\n  position: absolute;\n  top: 0;\n  right: 0;\n  left: 0;\n  will-change: transform, filter;\n  filter: drop-shadow(0 0 0 rgba(0, 0, 0, 0));\n  transition: filter 240ms ease;\n}\n\n.shredder__item[data-state='drag'] {\n  cursor: grabbing;\n  filter: drop-shadow(0 14px 22px rgba(0, 0, 0, 0.22));\n}\n\n.shredder__item[data-state='feed'] {\n  cursor: default;\n  pointer-events: none;\n}\n\n.shredder[data-disabled] .shredder__item {\n  cursor: default;\n}\n\n.shredder__slit {\n  position: absolute;\n  right: var(--sh-slit-inset);\n  bottom: var(--sh-fall);\n  left: var(--sh-slit-inset);\n  z-index: 3;\n  height: var(--sh-slit-h);\n  border-radius: 999px;\n  background: var(--sh-slit);\n  will-change: transform;\n}\n\n.shredder__fall {\n  position: absolute;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 2;\n  height: var(--sh-fall);\n  pointer-events: none;\n}\n\n.shredder__canvas {\n  display: block;\n  position: absolute;\n  top: 0;\n  left: calc(-1 * var(--sh-over));\n  width: calc(100% + var(--sh-over) * 2);\n  height: 100%;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .shredder__slot[data-active] .shredder__item {\n    transition: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "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\nimport './Shredder.css';\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={`shredder${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=\"shredder__list\">\n        {sorted.map((item, index) => (\n          <li key={item.id} ref={keep(slotEls, item.id)} className=\"shredder__slot\">\n            <div\n              ref={keep(itemEls, item.id)}\n              className=\"shredder__item\"\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 ref={slitRef} className=\"shredder__slit\" aria-hidden=\"true\" />\n      <div className=\"shredder__fall\" aria-hidden=\"true\">\n        <canvas ref={canvasRef} className=\"shredder__canvas\" />\n      </div>\n    </div>\n  );\n};\n\nexport default Shredder;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"react-dom@^19.0.0"
	]
}