{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "SwipeToast-JS-CSS",
	"title": "SwipeToast",
	"description": "Toast that swipes down to dismiss and burns a thin fuse for its remaining time.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "SwipeToast.css",
			"target": "@components/SwipeToast.css",
			"content": ".swipe-toast {\n  --st-bg: #27272a;\n  --st-ink: #f5f5f5;\n  --st-fuse: #f5a524;\n  --st-fuse-h: 2px;\n  --st-w: 356px;\n  --st-radius: 12px;\n  --st-slide: 400ms;\n  --st-gap: 10px;\n  --st-offset: 32px;\n  --st-z: 999999999;\n  --st-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n\n  display: grid;\n  grid-template-rows: 1fr;\n  width: min(var(--st-w), 100%);\n  color: var(--st-ink);\n  font-family: inherit;\n  font-size: 13px;\n  line-height: 1.5;\n}\n\n.swipe-toast[data-inline='false'] {\n  position: fixed;\n  right: var(--st-offset);\n  bottom: calc(var(--st-offset) + env(safe-area-inset-bottom, 0px));\n  z-index: var(--st-z);\n  width: min(var(--st-w), calc(100vw - 2 * var(--st-offset)));\n}\n\n@media (max-width: 600px) {\n  .swipe-toast[data-inline='false'] {\n    --st-offset: 16px;\n  }\n}\n\n.swipe-toast[data-inline='true'] {\n  transition: grid-template-rows var(--st-slide) var(--st-ease-out);\n}\n\n.swipe-toast[data-inline='true'][data-mounted='false'] {\n  grid-template-rows: 0fr;\n}\n\n.swipe-toast[data-inline='true'][data-phase='closing'],\n.swipe-toast[data-inline='true'][data-phase='gone'] {\n  grid-template-rows: 0fr;\n}\n\n.swipe-toast[data-inline='true'][data-phase='closing'] {\n  transition-duration: calc(var(--st-slide) * 0.7);\n}\n\n.swipe-toast[data-inline='true'][data-phase='gone'] {\n  transition-duration: 200ms;\n}\n\n.swipe-toast__gate {\n  min-height: 0;\n}\n\n.swipe-toast[data-inline='true'] .swipe-toast__lift {\n  margin-top: var(--st-gap);\n}\n\n.swipe-toast__lift {\n  opacity: 1;\n  transform: translateY(0);\n  transition:\n    transform var(--st-slide) var(--st-ease-out),\n    opacity calc(var(--st-slide) * 0.6) ease;\n}\n\n@starting-style {\n  .swipe-toast[data-inline='true'] {\n    grid-template-rows: 0fr;\n  }\n\n  .swipe-toast__lift {\n    opacity: 0;\n    transform: translateY(100%);\n  }\n}\n\n.swipe-toast[data-mounted='false'] .swipe-toast__lift {\n  opacity: 0;\n  transform: translateY(100%);\n}\n\n.swipe-toast[data-phase='closing'] .swipe-toast__lift {\n  opacity: 0;\n  transform: translateY(100%);\n  transition:\n    transform calc(var(--st-slide) * 0.7) var(--st-ease-out),\n    opacity calc(var(--st-slide) * 0.5) ease;\n}\n\n.swipe-toast[data-phase='gone'] .swipe-toast__lift {\n  opacity: 0;\n  visibility: hidden;\n  transform: translateY(100%);\n}\n\n.swipe-toast[data-instant] .swipe-toast__lift {\n  transition-duration: 0s;\n}\n\n.swipe-toast__card {\n  position: relative;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 16px;\n  border-radius: var(--st-radius);\n  overflow: hidden;\n  background: var(--st-bg);\n  box-shadow:\n    0 1px 2px rgba(0, 0, 0, 0.05),\n    0 4px 12px rgba(0, 0, 0, 0.1);\n  cursor: grab;\n  outline: none;\n  touch-action: none;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-touch-callout: none;\n  -webkit-tap-highlight-color: transparent;\n}\n\n.swipe-toast__card[data-swiping] {\n  cursor: grabbing;\n}\n\n.swipe-toast[data-dismissible='false'] .swipe-toast__card {\n  cursor: default;\n  touch-action: auto;\n}\n\n.swipe-toast__icon {\n  display: inline-flex;\n  flex: none;\n  width: 18px;\n  height: 18px;\n}\n\n.swipe-toast__icon svg {\n  width: 100%;\n  height: 100%;\n}\n\n.swipe-toast__body {\n  display: flex;\n  flex: 1 1 auto;\n  flex-direction: column;\n  gap: 1px;\n  min-width: 0;\n}\n\n.swipe-toast__title {\n  font-weight: 500;\n  line-height: 1.5;\n}\n\n.swipe-toast__desc {\n  color: color-mix(in srgb, var(--st-ink) 66%, transparent);\n  font-weight: 400;\n  line-height: 1.4;\n}\n\n.swipe-toast__action,\n.swipe-toast__close {\n  flex: none;\n  border: 0;\n  font: inherit;\n  cursor: pointer;\n  outline: none;\n  touch-action: manipulation;\n  -webkit-tap-highlight-color: transparent;\n  transition:\n    transform 160ms var(--st-ease-out),\n    background-color 160ms ease,\n    opacity 160ms ease;\n}\n\n.swipe-toast__action {\n  height: 24px;\n  padding: 0 8px;\n  border-radius: 5px;\n  background: var(--st-ink);\n  color: var(--st-bg);\n  font-size: 12px;\n  font-weight: 500;\n}\n\n.swipe-toast__close {\n  position: relative;\n  display: grid;\n  place-items: center;\n  width: 24px;\n  height: 24px;\n  border-radius: 5px;\n  background: transparent;\n  color: inherit;\n}\n\n.swipe-toast__close::before {\n  content: '';\n  position: absolute;\n  inset: -8px;\n}\n\n.swipe-toast__action:active,\n.swipe-toast__close:active {\n  transform: scale(0.97);\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .swipe-toast__action:hover {\n    opacity: 0.88;\n  }\n\n  .swipe-toast__close:hover {\n    background: color-mix(in srgb, var(--st-ink) 10%, transparent);\n  }\n}\n\n.swipe-toast__fuse {\n  position: absolute;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  height: var(--st-fuse-h);\n  background: linear-gradient(\n    90deg,\n    transparent 0%,\n    color-mix(in srgb, var(--st-fuse) 18%, transparent) 9%,\n    color-mix(in srgb, var(--st-fuse) 50%, transparent) 18%,\n    color-mix(in srgb, var(--st-fuse) 84%, transparent) 28%,\n    var(--st-fuse) 38%,\n    var(--st-fuse) 62%,\n    color-mix(in srgb, var(--st-fuse) 84%, transparent) 72%,\n    color-mix(in srgb, var(--st-fuse) 50%, transparent) 82%,\n    color-mix(in srgb, var(--st-fuse) 18%, transparent) 91%,\n    transparent 100%\n  );\n  transform-origin: left center;\n  pointer-events: none;\n}\n\n.swipe-toast[data-fuse='top'] .swipe-toast__fuse {\n  top: 0;\n  bottom: auto;\n}\n\n.swipe-toast[data-fuse='none'] .swipe-toast__fuse {\n  opacity: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .swipe-toast__lift {\n    transform: none !important;\n    transition: opacity 200ms ease;\n  }\n\n  .swipe-toast__action:active,\n  .swipe-toast__close:active {\n    transform: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "SwipeToast.jsx",
			"content": "'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport { animate, motion, useMotionValue, useReducedMotion, useTransform } from 'motion/react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { Cancel01Icon } from '@hugeicons/core-free-icons';\n\nimport './SwipeToast.css';\n\nconst EASE_OUT = [0.23, 1, 0.32, 1];\nconst FLICK = 0.11;\nconst DEAD_ZONE = 3;\nconst RESIST_PX = 24;\nconst COLLAPSE_MS = 200;\nconst EXIT = 0.7;\nconst BURN = [{ transform: 'scaleX(1)' }, { transform: 'scaleX(0)' }];\nconst HAS_STARTING_STYLE = typeof window !== 'undefined' && 'CSSStartingStyleRule' in window;\n\nconst rubberband = (over, dim, c = 0.55) => (over * dim * c) / (dim + c * Math.abs(over));\nconst velocityOf = hist => {\n  if (hist.length < 2) return 0;\n  const [t0, y0] = hist[0];\n  const [t1, y1] = hist[hist.length - 1];\n  return performance.now() - t1 > 100 ? 0 : (y1 - y0) / Math.max(1, t1 - t0);\n};\n\nexport default function SwipeToast({\n  title = 'File archived',\n  description = '',\n  icon,\n  actionLabel = '',\n  onAction,\n  open = true,\n  onClose,\n  background = '#27272a',\n  color = '#f5f5f5',\n  fuseColor = '#f5a524',\n  width = 356,\n  radius = 12,\n  slideMs = 400,\n  settleBounce = 0.2,\n  swipeDistance = 40,\n  duration = 4000,\n  fuse = 'bottom',\n  pauseOnHover = true,\n  closeButton = false,\n  inline = false,\n  dismissible = true,\n  className = ''\n}) {\n  const reduce = useReducedMotion();\n  const [phase, setPhase] = useState('open');\n  const [instant, setInstant] = useState(false);\n  const [mounted, setMounted] = useState(HAS_STARTING_STYLE);\n  const cardRef = useRef(null);\n  const fuseRef = useRef(null);\n  const anim = useRef(null);\n  const drag = useRef(null);\n  const flags = useRef({ hover: false, interacting: false, focus: false, hidden: false });\n  const lastInput = useRef('pointer');\n  const pendingClose = useRef(null);\n  const closeTimer = useRef(undefined);\n  const reason = useRef('timeout');\n  const leaving = useRef(false);\n  const phaseRef = useRef(phase);\n  phaseRef.current = phase;\n  const latest = useRef({});\n  latest.current = { onClose, onAction, slideMs, inline };\n\n  const y = useMotionValue(0);\n  const fade = useMotionValue(1);\n  const transform = useTransform(y, v => `translateY(${v}px)`);\n\n  const syncFuse = () => {\n    const a = anim.current;\n    if (!a) return;\n    const f = flags.current;\n    if (f.hover || f.interacting || f.focus || f.hidden) a.pause();\n    else if (a.playState === 'paused') a.play();\n  };\n  const finish = why => {\n    setPhase('gone');\n    leaving.current = false;\n    if (latest.current.inline) closeTimer.current = setTimeout(() => latest.current.onClose?.(why), COLLAPSE_MS);\n    else latest.current.onClose?.(why);\n  };\n  const close = why => {\n    if (phaseRef.current !== 'open' || leaving.current) return;\n    if (drag.current) {\n      pendingClose.current = why;\n      return;\n    }\n    anim.current?.pause();\n    reason.current = why;\n    const now = why === 'escape' || ((why === 'action' || why === 'close') && lastInput.current === 'keyboard');\n    setInstant(now);\n    setPhase('closing');\n    clearTimeout(closeTimer.current);\n    closeTimer.current = setTimeout(() => finish(why), now ? 0 : latest.current.slideMs * EXIT + 60);\n  };\n  const rescue = () => {\n    clearTimeout(closeTimer.current);\n    setInstant(false);\n    y.set(0);\n    fade.set(1);\n    setPhase('open');\n  };\n\n  useEffect(() => {\n    if (!open) close('programmatic');\n    else if (phaseRef.current !== 'open') rescue();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [open]);\n  useEffect(() => {\n    if (!HAS_STARTING_STYLE) requestAnimationFrame(() => setMounted(true));\n  }, []);\n  useEffect(() => {\n    if (phase !== 'open' || duration <= 0 || !fuseRef.current) return undefined;\n    anim.current?.cancel();\n    const a = fuseRef.current.animate(BURN, { duration, easing: 'linear', fill: 'forwards' });\n    a.onfinish = () => close('timeout');\n    anim.current = a;\n    syncFuse();\n    return () => {\n      a.onfinish = null;\n      a.pause();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [phase, duration]);\n  useEffect(() => {\n    if (!pauseOnHover) {\n      flags.current.hover = false;\n      syncFuse();\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [pauseOnHover]);\n  useEffect(() => {\n    const onVisibility = () => {\n      flags.current.hidden = document.hidden;\n      syncFuse();\n    };\n    document.addEventListener('visibilitychange', onVisibility);\n    return () => {\n      document.removeEventListener('visibilitychange', onVisibility);\n      clearTimeout(closeTimer.current);\n      anim.current?.cancel();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const swipeOut = (dy, v) => {\n    anim.current?.pause();\n    leaving.current = true;\n    pendingClose.current = null;\n    if (!reduce && cardRef.current) {\n      animate(y, dy + cardRef.current.offsetHeight, { type: 'spring', duration: 0.3, bounce: 0, velocity: v * 1000 });\n    }\n    animate(fade, 0, { duration: 0.2, ease: EASE_OUT }).then(() => finish('swipe'));\n  };\n  const onPointerDown = e => {\n    lastInput.current = 'pointer';\n    if (e.button !== 0 || !dismissible || drag.current || leaving.current || e.target.closest('button')) return;\n    if (phaseRef.current === 'closing') rescue();\n    try {\n      cardRef.current?.setPointerCapture(e.pointerId);\n    } catch {}\n    y.stop();\n    drag.current = {\n      id: e.pointerId,\n      startY: e.clientY,\n      grab: null,\n      moved: false,\n      hist: [[performance.now(), y.get()]]\n    };\n    flags.current.interacting = true;\n    syncFuse();\n  };\n  const onPointerMove = e => {\n    const d = drag.current;\n    if (!d || d.id !== e.pointerId) return;\n    if (d.grab === null) {\n      if (Math.abs(e.clientY - d.startY) < DEAD_ZONE) return;\n      d.grab = e.clientY - y.get();\n      if (cardRef.current) cardRef.current.dataset.swiping = '';\n    }\n    const raw = e.clientY - d.grab;\n    const next = raw >= 0 ? raw : rubberband(raw, RESIST_PX);\n    y.set(next);\n    d.moved = true;\n    d.hist.push([performance.now(), next]);\n    if (d.hist.length > 4) d.hist.shift();\n  };\n  const onPointerUp = e => {\n    const d = drag.current;\n    if (!d || d.id !== e.pointerId) return;\n    drag.current = null;\n    if (cardRef.current) delete cardRef.current.dataset.swiping;\n    try {\n      cardRef.current?.releasePointerCapture(e.pointerId);\n    } catch {}\n    flags.current.interacting = false;\n    const dy = y.get();\n    const v = velocityOf(d.hist);\n    if (dy > 0 && (v > FLICK || (dy >= swipeDistance && v >= 0))) {\n      swipeOut(dy, v);\n      return;\n    }\n    if (d.moved) {\n      animate(\n        y,\n        0,\n        reduce\n          ? { duration: 0.2, ease: EASE_OUT }\n          : { type: 'spring', duration: 0.5, bounce: settleBounce, velocity: v * 1000 }\n      );\n    }\n    const queued = pendingClose.current;\n    pendingClose.current = null;\n    if (queued) close(queued);\n    else syncFuse();\n  };\n\n  return (\n    <div\n      className={`swipe-toast${className ? ` ${className}` : ''}`}\n      data-phase={phase}\n      data-inline={inline ? 'true' : 'false'}\n      data-fuse={duration > 0 ? fuse : 'none'}\n      data-dismissible={dismissible ? 'true' : 'false'}\n      data-instant={instant ? '' : undefined}\n      data-mounted={mounted ? 'true' : 'false'}\n      style={{\n        '--st-bg': background,\n        '--st-ink': color,\n        '--st-fuse': fuseColor,\n        '--st-w': `${width}px`,\n        '--st-radius': `${radius}px`,\n        '--st-slide': `${slideMs}ms`,\n        '--st-gap': '10px'\n      }}\n    >\n      <div className=\"swipe-toast__gate\">\n        <div className=\"swipe-toast__lift\">\n          <motion.div\n            ref={cardRef}\n            className=\"swipe-toast__card\"\n            role=\"status\"\n            aria-live=\"polite\"\n            aria-atomic=\"true\"\n            tabIndex={0}\n            style={{ transform, opacity: fade }}\n            onPointerDown={onPointerDown}\n            onPointerMove={onPointerMove}\n            onPointerUp={onPointerUp}\n            onPointerCancel={onPointerUp}\n            onPointerEnter={e => {\n              if (pauseOnHover && e.pointerType === 'mouse') {\n                flags.current.hover = true;\n                syncFuse();\n              }\n            }}\n            onPointerLeave={e => {\n              if (e.pointerType === 'mouse') {\n                flags.current.hover = false;\n                syncFuse();\n              }\n            }}\n            onFocus={() => {\n              flags.current.focus = true;\n              syncFuse();\n            }}\n            onBlur={e => {\n              if (!e.currentTarget.contains(e.relatedTarget)) {\n                flags.current.focus = false;\n                syncFuse();\n              }\n            }}\n            onKeyDown={e => {\n              if (e.key === 'Enter' || e.key === ' ') lastInput.current = 'keyboard';\n              if (e.key === 'Escape' && dismissible) {\n                e.stopPropagation();\n                close('escape');\n              }\n            }}\n          >\n            {icon ? (\n              <span className=\"swipe-toast__icon\" aria-hidden=\"true\">\n                {icon}\n              </span>\n            ) : null}\n            <span className=\"swipe-toast__body\">\n              <span className=\"swipe-toast__title\">{title}</span>\n              {description ? <span className=\"swipe-toast__desc\">{description}</span> : null}\n            </span>\n            {actionLabel ? (\n              <button\n                type=\"button\"\n                className=\"swipe-toast__action\"\n                onClick={() => {\n                  latest.current.onAction?.();\n                  close('action');\n                }}\n              >\n                {actionLabel}\n              </button>\n            ) : null}\n            {closeButton ? (\n              <button type=\"button\" className=\"swipe-toast__close\" aria-label=\"Close\" onClick={() => close('close')}>\n                <HugeiconsIcon icon={Cancel01Icon} size={12} strokeWidth={2.5} />\n              </button>\n            ) : null}\n            <i ref={fuseRef} className=\"swipe-toast__fuse\" aria-hidden=\"true\" />\n          </motion.div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12",
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}