{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"strip-delete","type":"registry:ui","title":"Strip Delete","description":"Delete a document card and it feeds into a rattling shredder, coming out as strips that scatter and fall, with an undo that drops it back in.","dependencies":["motion","lucide-react"],"registryDependencies":[],"files":[{"path":"registry/ui/strip-delete.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { FileText, RotateCcw, Trash2 } from \"lucide-react\";\n\nexport interface StripDeleteProps {\n  /** Document name on the card. */\n  title?: string;\n  /** Line under the title. */\n  subtitle?: string;\n  /** Custom card body (defaults to a skeleton page). */\n  children?: ReactNode;\n  /** Number of strips the card is cut into. */\n  strips?: number;\n  /** Ms the undo button stays available. 0 keeps it forever. */\n  undoTimeout?: number;\n  /** Called when the card is shredded. */\n  onDelete?: () => void;\n  /** Called when undo brings it back. */\n  onUndo?: () => void;\n  /** Called when the undo window closes. */\n  onExpire?: () => void;\n  className?: string;\n}\n\ntype Phase = \"idle\" | \"shredding\" | \"gone\" | \"expired\";\nconst CARD_H = 176;\nconst SHRED_MS = 1500;\n\n// Deterministic jitter so SSR and client agree.\nconst rand = (i: number, salt: number) => {\n  const x = Math.sin(i * 12.9898 + salt * 78.233) * 43758.5453;\n  return x - Math.floor(x);\n};\n\nexport function StripDelete({\n  title = \"Q3-forecast.pdf\",\n  subtitle = \"2.4 MB · edited 3h ago\",\n  children,\n  strips = 12,\n  undoTimeout = 5000,\n  onDelete,\n  onUndo,\n  onExpire,\n  className,\n}: StripDeleteProps) {\n  const reduce = useReducedMotion();\n  const [phase, setPhase] = useState<Phase>(\"idle\");\n  const [round, setRound] = useState(0);\n  const timers = useRef<number[]>([]);\n  const undoBtn = useRef<HTMLButtonElement>(null);\n  const deleteBtn = useRef<HTMLButtonElement>(null);\n  useEffect(() => () => timers.current.forEach(clearTimeout), []);\n\n  const later = (ms: number, fn: () => void) => timers.current.push(window.setTimeout(fn, ms));\n\n  const shred = () => {\n    if (phase !== \"idle\") return;\n    setPhase(\"shredding\");\n    onDelete?.();\n    later(reduce ? 200 : SHRED_MS, () => {\n      setPhase(\"gone\");\n      requestAnimationFrame(() => undoBtn.current?.focus());\n      if (undoTimeout > 0)\n        later(undoTimeout, () => {\n          setPhase(\"expired\");\n          onExpire?.();\n        });\n    });\n  };\n  const undo = () => {\n    timers.current.forEach(clearTimeout);\n    timers.current = [];\n    setRound((r) => r + 1);\n    setPhase(\"idle\");\n    onUndo?.();\n    requestAnimationFrame(() => deleteBtn.current?.focus());\n  };\n\n  const body = children ?? (\n    <div className=\"flex h-full flex-col gap-3 p-4\">\n      <div className=\"flex items-center gap-3\">\n        <span className=\"grid size-9 place-items-center rounded-lg bg-primary/15 text-primary\">\n          <FileText className=\"size-4.5\" aria-hidden />\n        </span>\n        <div className=\"min-w-0\">\n          <p className=\"truncate text-sm font-semibold text-foreground\">{title}</p>\n          <p className=\"text-xs text-muted-foreground\">{subtitle}</p>\n        </div>\n      </div>\n      <div className=\"grid gap-1.5 pt-1\">\n        {[100, 92, 97, 64, 88, 40].map((w, i) => (\n          <span key={i} className=\"h-1.5 rounded-full bg-muted\" style={{ width: `${w}%` }} />\n        ))}\n      </div>\n    </div>\n  );\n\n  return (\n    <div className={`relative w-72 ${className ?? \"\"}`} style={{ height: CARD_H + 230 }}>\n      {/* Card: whole while idle, a stack of adjacent strips while shredding. */}\n      <div className=\"absolute inset-x-3 top-0\" style={{ height: CARD_H }}>\n        <AnimatePresence initial={false}>\n          {phase === \"idle\" && (\n            <motion.div\n              key={`card-${round}`}\n              className=\"absolute inset-0 overflow-hidden rounded-xl border border-border bg-card shadow-xl\"\n              initial={round ? { y: -40, scale: 0.85, opacity: 0 } : false}\n              animate={{ y: 0, scale: 1, opacity: 1 }}\n              transition={{ type: \"spring\", stiffness: 420, damping: 16 }}\n            >\n              {body}\n              <motion.button\n                ref={deleteBtn}\n                type=\"button\"\n                onClick={shred}\n                aria-label={`Shred ${title}`}\n                whileHover={{ scale: 1.08, rotate: -6 }}\n                whileTap={{ scale: 0.85 }}\n                className=\"absolute bottom-3 right-3 grid size-8 place-items-center rounded-lg text-muted-foreground outline-none hover:bg-destructive/10 hover:text-destructive focus-visible:ring-2 focus-visible:ring-ring\"\n              >\n                <Trash2 className=\"size-4\" aria-hidden />\n              </motion.button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        {phase === \"shredding\" &&\n          !reduce &&\n          Array.from({ length: strips }, (_, i) => {\n            const a = (i / strips) * 100;\n            const b = 100 - ((i + 1) / strips) * 100;\n            const drift = (rand(i, round) - 0.5) * 70;\n            return (\n              <motion.div\n                key={i}\n                aria-hidden\n                className=\"absolute inset-0 overflow-hidden rounded-xl border border-border bg-card\"\n                style={{ clipPath: `inset(0 ${b}% 0 ${a}%)` }}\n                initial={{ y: 0 }}\n                animate={{\n                  y: [0, CARD_H + 56, CARD_H + 56 + 150 + rand(i, 7) * 60],\n                  x: [0, 0, drift],\n                  rotate: [0, 0, drift / 3],\n                  opacity: [1, 1, 0],\n                }}\n                transition={{ duration: 1.3, times: [0, 0.55, 1], ease: [\"easeIn\", \"easeIn\"], delay: rand(i, 3) * 0.12 }}\n              >\n                {body}\n              </motion.div>\n            );\n          })}\n      </div>\n\n      {/* The machine sits in front of the strips so they vanish into its mouth. */}\n      <motion.div\n        aria-hidden\n        className=\"absolute inset-x-0 z-10 h-14 rounded-2xl border border-border bg-muted shadow-[inset_0_1px_0_rgb(255_255_255/.08),0_10px_30px_-10px_rgb(0_0_0/.5)]\"\n        style={{ top: CARD_H - 4 }}\n        animate={phase === \"shredding\" && !reduce ? { x: [0, -1.5, 1.5, -1, 1, 0], y: [0, 1, 0, 1, 0] } : { x: 0, y: 0 }}\n        transition={phase === \"shredding\" ? { duration: 0.18, repeat: 6 } : undefined}\n      >\n        <div className=\"mx-3 mt-2 h-1.5 rounded-full bg-background shadow-[inset_0_1px_2px_rgb(0_0_0/.6)]\" />\n        <div className=\"mx-auto mt-2.5 flex w-fit items-center gap-1.5\">\n          <span className={`size-1.5 rounded-full transition-colors ${phase === \"shredding\" ? \"bg-red-500\" : \"bg-emerald-500\"}`} />\n          <span className=\"text-[10px] font-medium uppercase tracking-widest text-muted-foreground\">\n            {phase === \"shredding\" ? \"Shredding\" : \"Ready\"}\n          </span>\n        </div>\n      </motion.div>\n\n      <div className=\"absolute inset-x-0 grid place-items-center\" style={{ top: CARD_H + 72 }}>\n        <AnimatePresence>\n          {phase === \"gone\" && (\n            <motion.div\n              initial={{ opacity: 0, y: 10, scale: 0.9 }}\n              animate={{ opacity: 1, y: 0, scale: 1 }}\n              exit={{ opacity: 0, scale: 0.9 }}\n              transition={{ type: \"spring\", stiffness: 500, damping: 26 }}\n              className=\"flex items-center gap-3 rounded-full border border-border bg-card py-1.5 pl-4 pr-1.5 text-sm text-foreground shadow-lg\"\n            >\n              <span>Document shredded</span>\n              <button\n                ref={undoBtn}\n                type=\"button\"\n                onClick={undo}\n                className=\"group flex items-center gap-1.5 rounded-full bg-primary px-3 py-1 text-xs font-semibold text-primary-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card\"\n              >\n                <RotateCcw className=\"size-3.5 transition-transform duration-300 group-hover:-rotate-180\" aria-hidden />\n                Undo\n              </button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {phase === \"gone\" ? `${title} shredded. Undo available.` : \"\"}\n      </span>\n    </div>\n  );\n}\n"}]}