{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "delete-button",
  "title": "Delete button",
  "description": "A two-step delete (Delete, then Confirm with an X to back out) that ends in a Thanos snap: the button tears into dust through an SVG displacement filter. Ships with ThanosSnapEffect, a wrapper that snaps anything else the same way.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/items/delete-button/delete-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotionConfig } from \"motion/react\";\nimport { Check, Loader2, Trash2, X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useThanosSnap } from \"./thanos-snap-effect\";\n\nexport type DeleteButtonSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n/** `solid`: a filled destructive button. `subtle`: a washed one that sits quietly inside a card. */\nexport type DeleteButtonVariant = \"solid\" | \"subtle\";\n\nexport interface DeleteButtonProps {\n  /** Resting label. Defaults to \"Delete\". */\n  label?: React.ReactNode;\n  /** Label once armed. Defaults to \"Confirm\". */\n  confirmLabel?: React.ReactNode;\n  /** Label while the delete runs. Defaults to \"Deleting…\". */\n  deletingLabel?: React.ReactNode;\n  /** Milliseconds \"Deleting…\" holds before the snap starts. Defaults to 650. */\n  deletingDuration?: number;\n  /**\n   * The delete. Runs alongside the snap; the button waits for both. Handle\n   * your own failures inside it (show a message, then remount the button\n   * through its `key`): the snap cannot know in advance that it will fail.\n   */\n  onConfirm?: () => void | Promise<void>;\n  /** The user backed out, by the X, Escape, or the auto-reset timeout. */\n  onCancel?: () => void;\n  /** The button armed itself (Delete → Confirm). */\n  onStartConfirm?: () => void;\n  /**\n   * The dissolve has finished. Unmount the deleted row here, not in\n   * `onConfirm`: removing the element while the snap plays cuts it dead.\n   */\n  onComplete?: () => void;\n  /** `onConfirm` threw. The button comes back idle. Without this, the error is rethrown. */\n  onError?: (error: unknown) => void;\n  /** Defaults to \"solid\". */\n  variant?: DeleteButtonVariant;\n  /** Defaults to \"md\". */\n  size?: DeleteButtonSize;\n  /** Dissolve to dust on confirm. Defaults to true; false makes it a plain two-step confirm. */\n  snapOnConfirm?: boolean;\n  /** Stay gone once done. Defaults to true; false returns it to idle. */\n  vanishOnComplete?: boolean;\n  disabled?: boolean;\n  /** Holds the button disabled from outside, e.g. while a parent's request is in flight. */\n  loading?: boolean;\n  /** Milliseconds before an armed button disarms itself. 0 (the default) never does. */\n  autoResetTimeout?: number;\n  /** Classes for the row holding both buttons. */\n  className?: string;\n  /** Classes for the delete/confirm button. */\n  buttonClassName?: string;\n  /** Classes for the X. */\n  cancelButtonClassName?: string;\n  \"aria-label\"?: string;\n  /** Defaults to \"Cancel deletion\". */\n  cancelAriaLabel?: string;\n}\n\n/**\n * Every part of the confirm/cancel morph (button width, label swap, icon swap,\n * the X sliding in and out) runs on this one curve so the group reads as a\n * single continuous movement instead of a sequence of separate steps.\n */\nconst MORPH_EASE: [number, number, number, number] = [0.22, 1, 0.36, 1];\nconst MORPH_TRANSITION = { duration: 0.3, ease: MORPH_EASE };\nconst SWAP_TRANSITION = { duration: 0.22, ease: MORPH_EASE };\n\nconst sizeConfig = {\n  xs: { button: \"h-7 px-2.5 text-xs gap-1.5\", iconSize: 13, cancelBtn: \"h-7 min-w-[32px] px-2\", gapPx: 6 },\n  sm: { button: \"h-8 px-3 text-xs font-medium gap-1.5\", iconSize: 14, cancelBtn: \"h-8 min-w-[38px] px-2.5\", gapPx: 6 },\n  md: { button: \"h-9 px-3.5 text-sm font-medium gap-2\", iconSize: 16, cancelBtn: \"h-9 min-w-[42px] px-3\", gapPx: 8 },\n  lg: { button: \"h-10 px-4 text-sm font-semibold gap-2.5\", iconSize: 18, cancelBtn: \"h-10 min-w-[46px] px-3.5\", gapPx: 10 },\n} satisfies Record<DeleteButtonSize, { button: string; iconSize: number; cancelBtn: string; gapPx: number }>;\n\nconst CANCEL_STYLE =\n  \"bg-transparent text-dim hover:text-ink hover:bg-paper active:bg-paper-sunk border border-rule\";\nconst COMMITTED_STYLE = \"bg-stop text-on-solid shadow-xs border border-stop\";\n\nconst variantStyles: Record<\n  DeleteButtonVariant,\n  { idle: string; confirming: string; deleting: string; cancel: string }\n> = {\n  solid: {\n    idle: \"bg-stop hover:bg-stop/90 active:bg-stop/80 text-on-solid shadow-xs border border-stop\",\n    confirming:\n      \"bg-stop hover:bg-stop/90 active:bg-stop/80 text-on-solid shadow-xs ring-2 ring-stop/20 border border-stop\",\n    deleting: COMMITTED_STYLE,\n    cancel: CANCEL_STYLE,\n  },\n  subtle: {\n    idle: \"bg-stop-wash hover:bg-stop/15 active:bg-stop/20 text-stop border border-stop/25\",\n    confirming: \"bg-stop hover:bg-stop/90 active:bg-stop/80 text-on-solid shadow-xs border border-stop\",\n    deleting: COMMITTED_STYLE,\n    cancel: CANCEL_STYLE,\n  },\n};\n\ntype Status = \"idle\" | \"confirming\" | \"deleting\" | \"snapping\" | \"vanished\";\n\nconst wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * A two-step delete that ends in a Thanos snap.\n *\n * 1. Idle: \"Delete\" with a trash can.\n * 2. Pressed: it arms — \"Confirm\" with a tick — and an X slides out beside it.\n *    The X, Escape, or the optional timeout disarm it.\n * 3. Confirmed: \"Deleting…\" with a spinner for a beat, then the button\n *    dissolves to dust while `onConfirm` runs, and stays gone.\n *\n * The button dissolves itself. To take the row with it, remove the row in\n * `onComplete` (fold its height away), or wrap the row in `ThanosSnapEffect`\n * and snap that from `onConfirm` with `snapOnConfirm={false}`.\n */\nexport const DeleteButton = React.forwardRef<HTMLButtonElement, DeleteButtonProps>(\n  function DeleteButton(\n    {\n      label = \"Delete\",\n      confirmLabel = \"Confirm\",\n      deletingLabel = \"Deleting…\",\n      deletingDuration = 650,\n      onConfirm,\n      onCancel,\n      onStartConfirm,\n      onComplete,\n      onError,\n      variant = \"solid\",\n      size = \"md\",\n      snapOnConfirm = true,\n      vanishOnComplete = true,\n      disabled = false,\n      loading: externalLoading = false,\n      autoResetTimeout = 0,\n      className,\n      buttonClassName,\n      cancelButtonClassName,\n      \"aria-label\": ariaLabelProp,\n      cancelAriaLabel = \"Cancel deletion\",\n    },\n    ref,\n  ) {\n    const reduceMotion = useReducedMotionConfig();\n    const [status, setStatus] = React.useState<Status>(\"idle\");\n    const [internalLoading, setInternalLoading] = React.useState(false);\n    const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n    const { scope, targetRef, filterStyle, filter, snap, reset } = useThanosSnap({\n      duration: 0.65,\n      opacityStart: 0.45,\n    });\n\n    const isConfirming = status === \"confirming\";\n    const isDeleting = status === \"deleting\";\n    const isSnapping = status === \"snapping\";\n    const isExecuting = externalLoading || internalLoading || isDeleting || isSnapping;\n    /**\n     * Once the user commits, \"deleting\" and \"snapping\" must render\n     * *identically*. The dissolve has to eat the exact frame that was already\n     * on screen — if any style, icon or label branch told the two apart, the\n     * button would visibly revert at the instant the snap begins and the dust\n     * would be made of the reverted frame.\n     */\n    const isCommitted = isDeleting || isSnapping;\n\n    const currentSize = sizeConfig[size] ?? sizeConfig.md;\n    const currentVariant = variantStyles[variant] ?? variantStyles.solid;\n\n    const clearTimer = React.useCallback(() => {\n      if (timerRef.current) {\n        clearTimeout(timerRef.current);\n        timerRef.current = null;\n      }\n    }, []);\n\n    React.useEffect(() => clearTimer, [clearTimer]);\n\n    React.useEffect(() => {\n      if (!isConfirming || autoResetTimeout <= 0) return;\n      clearTimer();\n      timerRef.current = setTimeout(() => {\n        setStatus(\"idle\");\n        onCancel?.();\n      }, autoResetTimeout);\n      return clearTimer;\n    }, [isConfirming, autoResetTimeout, clearTimer, onCancel]);\n\n    React.useEffect(() => {\n      if (!isConfirming) return;\n      const handleKeyDown = (event: KeyboardEvent) => {\n        if (event.key !== \"Escape\") return;\n        event.preventDefault();\n        clearTimer();\n        setStatus(\"idle\");\n        onCancel?.();\n      };\n      window.addEventListener(\"keydown\", handleKeyDown);\n      return () => window.removeEventListener(\"keydown\", handleKeyDown);\n    }, [isConfirming, clearTimer, onCancel]);\n\n    const finish = () => {\n      setStatus(vanishOnComplete ? \"vanished\" : \"idle\");\n      if (!vanishOnComplete) reset();\n      setInternalLoading(false);\n      onComplete?.();\n    };\n\n    const fail = (error: unknown) => {\n      reset();\n      setStatus(\"idle\");\n      setInternalLoading(false);\n      if (onError) onError(error);\n      else throw error;\n    };\n\n    const handlePrimaryClick = async (event: React.MouseEvent<HTMLButtonElement>) => {\n      event.stopPropagation();\n      if (disabled || isExecuting || status === \"vanished\") return;\n\n      if (!isConfirming) {\n        setStatus(\"confirming\");\n        onStartConfirm?.();\n        return;\n      }\n\n      clearTimer();\n      setInternalLoading(true);\n      setStatus(\"deleting\");\n\n      if (!snapOnConfirm) {\n        try {\n          await onConfirm?.();\n        } catch (error) {\n          fail(error);\n          return;\n        }\n        finish();\n        return;\n      }\n\n      if (deletingDuration > 0) await wait(deletingDuration);\n      setStatus(\"snapping\");\n      // Let both settle: a rejected delete must not leave the button frozen\n      // half-dissolved. It finishes its snap, then comes back idle.\n      const [, outcome] = await Promise.allSettled([\n        snap(),\n        Promise.resolve().then(() => onConfirm?.()),\n      ]);\n      if (outcome.status === \"rejected\") {\n        fail(outcome.reason);\n        return;\n      }\n      finish();\n    };\n\n    const handleCancelClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n      event.stopPropagation();\n      if (disabled || isExecuting) return;\n      clearTimer();\n      setStatus(\"idle\");\n      onCancel?.();\n    };\n\n    if (status === \"vanished\") return null;\n\n    const buttonStyleClasses = isCommitted\n      ? currentVariant.deleting\n      : isConfirming\n        ? currentVariant.confirming\n        : currentVariant.idle;\n\n    // Icons turn as they swap; under reduced motion they only cross-fade.\n    const iconMotion = (turn: number, from: number) =>\n      reduceMotion\n        ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }\n        : {\n            initial: { opacity: 0, scale: from, rotate: -turn },\n            animate: { opacity: 1, scale: 1, rotate: 0 },\n            exit: { opacity: 0, scale: from, rotate: turn },\n          };\n    const labelMotion = reduceMotion\n      ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }\n      : { initial: { opacity: 0, y: 5 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -5 } };\n\n    return (\n      <div ref={scope} className=\"relative inline-block\">\n        <div ref={targetRef} style={filterStyle} className=\"will-change-transform\">\n          <div className={cn(\"inline-flex select-none items-center\", className)}>\n            <motion.button\n              ref={ref}\n              type=\"button\"\n              onClick={handlePrimaryClick}\n              disabled={disabled || isExecuting}\n              aria-label={\n                ariaLabelProp ||\n                (isConfirming ? \"Confirm deletion\" : isExecuting ? \"Deleting item\" : \"Delete item\")\n              }\n              aria-expanded={isConfirming}\n              aria-live=\"polite\"\n              whileTap={disabled || isExecuting || reduceMotion ? undefined : { scale: 0.97 }}\n              className={cn(\n                \"relative inline-flex cursor-pointer items-center justify-center overflow-hidden rounded-sm font-body font-medium tracking-tight transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stop/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none\",\n                // The button disables itself while deleting, so a blanket\n                // `disabled:opacity-50` would run the whole dissolve at half\n                // opacity and wash the dust out. Only dim a genuinely\n                // disabled control.\n                disabled && !isExecuting && \"opacity-50\",\n                currentSize.button,\n                buttonStyleClasses,\n                buttonClassName,\n              )}\n            >\n              {/* Trash → tick → spinner */}\n              <span className=\"relative flex shrink-0 items-center justify-center\">\n                <AnimatePresence mode=\"popLayout\" initial={false}>\n                  {isCommitted ? (\n                    <motion.span\n                      key=\"loading-icon\"\n                      {...iconMotion(45, 0.7)}\n                      transition={SWAP_TRANSITION}\n                      className=\"flex items-center justify-center\"\n                    >\n                      <Loader2 size={currentSize.iconSize} className=\"animate-spin\" aria-hidden=\"true\" />\n                    </motion.span>\n                  ) : isConfirming ? (\n                    <motion.span\n                      key=\"confirm-tick-icon\"\n                      {...iconMotion(20, 0.6)}\n                      transition={SWAP_TRANSITION}\n                      className=\"flex items-center justify-center\"\n                    >\n                      <Check size={currentSize.iconSize} strokeWidth={2.5} aria-hidden=\"true\" />\n                    </motion.span>\n                  ) : (\n                    <motion.span\n                      key=\"delete-trash-icon\"\n                      {...iconMotion(-20, 0.6)}\n                      transition={SWAP_TRANSITION}\n                      className=\"flex items-center justify-center\"\n                    >\n                      <Trash2 size={currentSize.iconSize} strokeWidth={2} aria-hidden=\"true\" />\n                    </motion.span>\n                  )}\n                </AnimatePresence>\n              </span>\n\n              {/* Delete → Confirm → Deleting… */}\n              <span className=\"relative inline-grid items-center justify-items-center overflow-hidden\">\n                {/* The executing label is usually the longest of the three, so\n                    it gets its own sizer that grows in on the shared curve\n                    instead of padding out the resting button. The grid column\n                    tracks the widest item, so animating this one animates the\n                    button. */}\n                <AnimatePresence initial={false}>\n                  {isCommitted && (\n                    <motion.span\n                      key=\"executing-sizer\"\n                      aria-hidden=\"true\"\n                      initial={{ width: 0 }}\n                      animate={{ width: \"auto\" }}\n                      exit={{ width: 0 }}\n                      transition={MORPH_TRANSITION}\n                      className=\"invisible col-start-1 row-start-1 overflow-hidden whitespace-nowrap font-semibold\"\n                    >\n                      {deletingLabel}\n                    </motion.span>\n                  )}\n                </AnimatePresence>\n                {/* Invisible sizers hold the column at the wider of the two\n                    resting labels, so swapping between them never resizes the\n                    button or nudges the row around it. */}\n                {[label, confirmLabel].map((sizerLabel, index) => (\n                  <span\n                    key={`label-sizer-${index}`}\n                    aria-hidden=\"true\"\n                    className=\"invisible col-start-1 row-start-1 whitespace-nowrap font-semibold\"\n                  >\n                    {sizerLabel}\n                  </span>\n                ))}\n                <span className=\"absolute inset-0 flex items-center justify-center\">\n                  <AnimatePresence mode=\"popLayout\" initial={false}>\n                    {isCommitted ? (\n                      <motion.span\n                        key=\"deleting-text\"\n                        {...labelMotion}\n                        transition={SWAP_TRANSITION}\n                        className=\"whitespace-nowrap font-medium\"\n                      >\n                        {deletingLabel}\n                      </motion.span>\n                    ) : isConfirming ? (\n                      <motion.span\n                        key=\"confirming-text\"\n                        {...labelMotion}\n                        transition={SWAP_TRANSITION}\n                        className=\"whitespace-nowrap font-semibold\"\n                      >\n                        {confirmLabel}\n                      </motion.span>\n                    ) : (\n                      <motion.span\n                        key=\"idle-text\"\n                        {...labelMotion}\n                        transition={SWAP_TRANSITION}\n                        className=\"whitespace-nowrap\"\n                      >\n                        {label}\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                </span>\n              </span>\n            </motion.button>\n\n            {/* The X: slides out beside Confirm, folds back on cancel. */}\n            <AnimatePresence initial={false}>\n              {isConfirming && !isExecuting && (\n                <motion.div\n                  key=\"cancel-x-button\"\n                  initial={{ width: 0, marginLeft: 0, opacity: 0 }}\n                  animate={{ width: \"auto\", marginLeft: currentSize.gapPx, opacity: 1 }}\n                  exit={{ width: 0, marginLeft: 0, opacity: 0 }}\n                  transition={MORPH_TRANSITION}\n                  className=\"shrink-0 overflow-hidden\"\n                >\n                  <motion.button\n                    type=\"button\"\n                    whileTap={reduceMotion ? undefined : { scale: 0.92 }}\n                    onClick={handleCancelClick}\n                    disabled={disabled || isExecuting}\n                    aria-label={cancelAriaLabel}\n                    title=\"Cancel\"\n                    className={cn(\n                      \"relative inline-flex shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-faint focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50\",\n                      currentSize.cancelBtn,\n                      currentVariant.cancel,\n                      cancelButtonClassName,\n                    )}\n                  >\n                    <X size={currentSize.iconSize} strokeWidth={2.2} aria-hidden=\"true\" />\n                  </motion.button>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </div>\n        </div>\n        {filter}\n      </div>\n    );\n  },\n);\n\nDeleteButton.displayName = \"DeleteButton\";\n\n/** The same component under the name some screens import it by. */\nexport const DeleteConfirmButton = DeleteButton;\nexport type DeleteConfirmButtonProps = DeleteButtonProps;\n",
      "type": "registry:component"
    },
    {
      "path": "registry/items/delete-button/thanos-snap-effect.tsx",
      "content": "\"use client\";\n\n/**\n * Highly inspired by Mikhail Bespalov's codepen\n * https://codepen.io/Mikhail-Bespalov/pen/yLmpxOG\n */\n\nimport * as React from \"react\";\nimport {\n  motion,\n  useAnimate,\n  useMotionValue,\n  useMotionValueEvent,\n  useReducedMotionConfig,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst MAX_DISPLACEMENT = 300;\n/** Ease-out cubic: the dust leaves fast and settles. */\nconst easeOutCubic = (time: number) => 1 - Math.pow(1 - time, 3);\n\ntype SnapTiming = {\n  /** Seconds the dissolve takes. */\n  duration: number;\n  /** Fraction of the duration the target stays opaque before fading. */\n  opacityStart: number;\n};\n\n/**\n * The dissolve itself, shared by the wrapper and the delete button: an SVG\n * filter whose displacement map is driven from 0 to 300 while the target\n * scales up and fades. Big low-frequency noise tears the element into\n * clumps; fine noise grinds the clumps into dust.\n *\n * Returns the refs to attach, a `filter` element to render once beside the\n * target, and `snap` / `reset`. Under reduced motion the snap is a short\n * fade with no displacement or growth.\n */\nexport function useThanosSnap({ duration, opacityStart }: SnapTiming) {\n  const reduceMotion = useReducedMotionConfig();\n  const reactId = React.useId();\n  const filterId = `dissolve-filter-${reactId.replace(/[^a-zA-Z0-9-_]/g, \"\")}`;\n\n  const [scope, animate] = useAnimate<HTMLDivElement>();\n  const targetRef = React.useRef<HTMLDivElement>(null);\n  const displacementMapRef = React.useRef<SVGFEDisplacementMapElement>(null);\n  const displacement = useMotionValue(0);\n\n  useMotionValueEvent(displacement, \"change\", (latest) => {\n    displacementMapRef.current?.setAttribute(\"scale\", latest.toString());\n  });\n\n  const snap = React.useCallback(async () => {\n    const target = targetRef.current;\n    if (!target) return;\n\n    if (reduceMotion) {\n      await animate(target, { opacity: 0 }, { duration: 0.2 });\n      return;\n    }\n\n    const transition = { duration, ease: easeOutCubic };\n    await Promise.all([\n      animate(\n        target,\n        { scale: 1.2, opacity: [1, 1, 0] },\n        { ...transition, times: [0, opacityStart, 1] },\n      ),\n      animate(displacement, MAX_DISPLACEMENT, transition),\n    ]);\n  }, [animate, displacement, duration, opacityStart, reduceMotion]);\n\n  const reset = React.useCallback(() => {\n    if (targetRef.current) {\n      animate(targetRef.current, { scale: 1, opacity: 1 }, { duration: 0 });\n    }\n    displacement.set(0);\n  }, [animate, displacement]);\n\n  const filter = (\n    <svg\n      aria-hidden=\"true\"\n      width=\"0\"\n      height=\"0\"\n      className=\"pointer-events-none absolute -z-10 opacity-0\"\n    >\n      <defs>\n        <filter\n          id={filterId}\n          x=\"-300%\"\n          y=\"-300%\"\n          width=\"600%\"\n          height=\"600%\"\n          colorInterpolationFilters=\"sRGB\"\n        >\n          <feTurbulence\n            type=\"fractalNoise\"\n            baseFrequency=\"0.015\"\n            numOctaves=\"1\"\n            result=\"bigNoise\"\n          />\n          <feComponentTransfer in=\"bigNoise\" result=\"bigNoiseAdjusted\">\n            <feFuncR type=\"linear\" slope=\"0.5\" intercept=\"-0.2\" />\n            <feFuncG type=\"linear\" slope=\"3\" intercept=\"-0.6\" />\n          </feComponentTransfer>\n          <feTurbulence\n            type=\"fractalNoise\"\n            baseFrequency=\"1\"\n            numOctaves=\"2\"\n            result=\"fineNoise\"\n          />\n          <feMerge result=\"combinedNoise\">\n            <feMergeNode in=\"bigNoiseAdjusted\" />\n            <feMergeNode in=\"fineNoise\" />\n          </feMerge>\n          <feDisplacementMap\n            ref={displacementMapRef}\n            in=\"SourceGraphic\"\n            in2=\"combinedNoise\"\n            scale=\"0\"\n            xChannelSelector=\"R\"\n            yChannelSelector=\"G\"\n          />\n        </filter>\n      </defs>\n    </svg>\n  );\n\n  return {\n    scope,\n    targetRef,\n    filterStyle: { filter: `url(#${filterId})` } as const,\n    filter,\n    snap,\n    reset,\n  };\n}\n\nexport interface ThanosSnapEffectProps extends React.PropsWithChildren {\n  /** Classes for the wrapper. It is `relative inline-block`; pass `block` to fill a row. */\n  className?: string;\n  /**\n   * Fires once the dissolve has finished. Remove the wrapped thing here, not\n   * before: unmounting it mid-snap cuts the dust off.\n   */\n  onComplete?: () => void;\n  /** Clicking the wrapped content snaps it. Defaults to true; turn off when a button inside drives it. */\n  triggerOnClick?: boolean;\n  /** Snaps when this turns true. */\n  trigger?: boolean;\n  /**\n   * Milliseconds after the snap before the content quietly reappears, ready to\n   * snap again. Defaults to 500. Pass `null` to leave it dissolved, which is\n   * what a delete wants while its row folds away.\n   */\n  resetDelay?: number | null;\n}\n\nexport interface ThanosSnapEffectRef {\n  /** Dissolves the content; resolves when the dust has settled. */\n  snap: () => Promise<void>;\n  /** Puts the content back at once. */\n  reset: () => void;\n}\n\n/**\n * Wraps anything and dissolves it to dust on demand: through the ref's\n * `snap()`, a `trigger` prop, or a click.\n */\nexport const ThanosSnapEffect = React.forwardRef<\n  ThanosSnapEffectRef,\n  ThanosSnapEffectProps\n>(function ThanosSnapEffect(\n  { children, className, onComplete, triggerOnClick = true, trigger, resetDelay = 500 },\n  ref,\n) {\n  const { scope, targetRef, filterStyle, filter, snap: dissolve, reset } =\n    useThanosSnap({ duration: 0.6, opacityStart: 0.5 });\n  const animating = React.useRef(false);\n  const resetTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  React.useEffect(\n    () => () => {\n      if (resetTimer.current) clearTimeout(resetTimer.current);\n    },\n    [],\n  );\n\n  const snap = React.useCallback(async () => {\n    if (animating.current) return;\n    animating.current = true;\n\n    await dissolve();\n    onComplete?.();\n\n    if (resetDelay === null) return;\n    resetTimer.current = setTimeout(() => {\n      reset();\n      animating.current = false;\n    }, resetDelay);\n  }, [dissolve, onComplete, reset, resetDelay]);\n\n  const resetNow = React.useCallback(() => {\n    if (resetTimer.current) clearTimeout(resetTimer.current);\n    reset();\n    animating.current = false;\n  }, [reset]);\n\n  React.useImperativeHandle(ref, () => ({ snap, reset: resetNow }), [snap, resetNow]);\n\n  React.useEffect(() => {\n    if (trigger) void snap();\n  }, [trigger, snap]);\n\n  return (\n    <div ref={scope} className={cn(\"relative inline-block\", className)}>\n      <motion.div\n        ref={targetRef}\n        onClick={triggerOnClick ? () => void snap() : undefined}\n        style={filterStyle}\n        className=\"will-change-transform\"\n      >\n        {children}\n      </motion.div>\n      {filter}\n    </div>\n  );\n});\n\nThanosSnapEffect.displayName = \"ThanosSnapEffect\";\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "kind": "widget",
    "whenToUse": "Use for deleting one row, card or version in place, where a modal confirm would be heavy and the user should feel the thing go. Remove the row in onComplete, never in onConfirm. Not for bulk deletes, for anything that cannot be undone and deserves a typed confirmation, or inside a container that clips overflow tightly, which cuts the dust off.",
    "origin": {
      "project": "180Connect",
      "path": "src/components/ui/delete-button.tsx (+ src/components/ui/thanos-snap-effect.tsx)"
    },
    "credits": [
      {
        "name": "Mikhail Bespalov, \"Thanos snap\" dissolve CodePen (the displacement-filter dissolve)",
        "url": "https://codepen.io/Mikhail-Bespalov/pen/yLmpxOG",
        "license": "MIT"
      }
    ]
  },
  "type": "registry:component"
}
