{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "SwipeToast-TS-TW",
	"title": "SwipeToast",
	"description": "Toast that swipes down to dismiss and burns a thin fuse for its remaining time.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "SwipeToast/SwipeToast.tsx",
			"content": "'use client';\n\nimport React, { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';\nimport { animate, motion, useMotionValue, useReducedMotion, useTransform } from 'motion/react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { Cancel01Icon } from '@hugeicons/core-free-icons';\n\nexport type SwipeToastCloseReason = 'timeout' | 'swipe' | 'action' | 'close' | 'escape' | 'programmatic';\nexport type SwipeToastFuse = 'bottom' | 'top' | 'none';\nexport type SwipeToastPhase = 'open' | 'closing' | 'gone';\n\nexport interface SwipeToastProps {\n  title?: ReactNode;\n  description?: ReactNode;\n  icon?: ReactNode;\n  actionLabel?: ReactNode;\n  onAction?: () => void;\n  open?: boolean;\n  onClose?: (reason: SwipeToastCloseReason) => void;\n  background?: string;\n  color?: string;\n  fuseColor?: string;\n  width?: number;\n  radius?: number;\n  slideMs?: number;\n  settleBounce?: number;\n  swipeDistance?: number;\n  duration?: number;\n  fuse?: SwipeToastFuse;\n  pauseOnHover?: boolean;\n  closeButton?: boolean;\n  inline?: boolean;\n  dismissible?: boolean;\n  className?: string;\n}\n\ntype Sample = [number, number];\n\ninterface Drag {\n  id: number;\n  startY: number;\n  grab: number | null;\n  moved: boolean;\n  hist: Sample[];\n}\n\ninterface Latest {\n  onClose?: (reason: SwipeToastCloseReason) => void;\n  onAction?: () => void;\n  slideMs: number;\n  inline: boolean;\n}\n\nconst EASE_OUT: [number, number, number, number] = [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: number, dim: number, c = 0.55) => (over * dim * c) / (dim + c * Math.abs(over));\nconst velocityOf = (hist: Sample[]) => {\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\nconst SwipeToast: React.FC<SwipeToastProps> = ({\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<SwipeToastPhase>('open');\n  const [instant, setInstant] = useState(false);\n  const [mounted, setMounted] = useState(HAS_STARTING_STYLE);\n  const cardRef = useRef<HTMLDivElement>(null);\n  const fuseRef = useRef<HTMLElement>(null);\n  const anim = useRef<Animation | null>(null);\n  const drag = useRef<Drag | null>(null);\n  const flags = useRef({ hover: false, interacting: false, focus: false, hidden: false });\n  const lastInput = useRef<'pointer' | 'keyboard'>('pointer');\n  const pendingClose = useRef<SwipeToastCloseReason | null>(null);\n  const closeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const reason = useRef<SwipeToastCloseReason>('timeout');\n  const leaving = useRef(false);\n  const phaseRef = useRef(phase);\n  phaseRef.current = phase;\n  const latest = useRef<Latest>({} as Latest);\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: SwipeToastCloseReason) => {\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: SwipeToastCloseReason) => {\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: number, v: number) => {\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: React.PointerEvent<HTMLDivElement>) => {\n    lastInput.current = 'pointer';\n    if (\n      e.button !== 0 ||\n      !dismissible ||\n      drag.current ||\n      leaving.current ||\n      (e.target as HTMLElement).closest('button')\n    )\n      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: React.PointerEvent<HTMLDivElement>) => {\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: React.PointerEvent<HTMLDivElement>) => {\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={`group grid w-[min(var(--st-w),100%)] text-[13px] leading-normal [grid-template-rows:1fr] [color:var(--st-ink)] data-[inline=false]:fixed data-[inline=false]:right-8 data-[inline=false]:bottom-[calc(32px+env(safe-area-inset-bottom,0px))] data-[inline=false]:z-[999999999] data-[inline=false]:w-[min(var(--st-w),calc(100vw-64px))] max-[600px]:data-[inline=false]:right-4 max-[600px]:data-[inline=false]:bottom-[calc(16px+env(safe-area-inset-bottom,0px))] max-[600px]:data-[inline=false]:w-[min(var(--st-w),calc(100vw-32px))] data-[inline=true]:[transition:grid-template-rows_var(--st-slide)_cubic-bezier(0.23,1,0.32,1)] data-[inline=true]:starting:[grid-template-rows:0fr] data-[inline=true]:data-[mounted=false]:[grid-template-rows:0fr] data-[inline=true]:data-[phase=closing]:[grid-template-rows:0fr] data-[inline=true]:data-[phase=gone]:[grid-template-rows:0fr] data-[inline=true]:data-[phase=closing]:[transition-duration:calc(var(--st-slide)*0.7)] data-[inline=true]:data-[phase=gone]:[transition-duration:200ms]${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        {\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        } as CSSProperties\n      }\n    >\n      <div className=\"min-h-0\">\n        <div className=\"opacity-100 group-data-[inline=true]:mt-[var(--st-gap)] [transform:translateY(0)] [transition:transform_var(--st-slide)_cubic-bezier(0.23,1,0.32,1),opacity_calc(var(--st-slide)*0.6)_ease] starting:opacity-0 starting:[transform:translateY(100%)] group-data-[mounted=false]:opacity-0 group-data-[mounted=false]:[transform:translateY(100%)] group-data-[phase=closing]:opacity-0 group-data-[phase=closing]:[transform:translateY(100%)] group-data-[phase=closing]:[transition:transform_calc(var(--st-slide)*0.7)_cubic-bezier(0.23,1,0.32,1),opacity_calc(var(--st-slide)*0.5)_ease] group-data-[phase=gone]:invisible group-data-[phase=gone]:opacity-0 group-data-[phase=gone]:[transform:translateY(100%)] group-data-[instant]:[transition-duration:0s] motion-reduce:[transform:none]! motion-reduce:[transition:opacity_200ms_ease]\">\n          <motion.div\n            ref={cardRef}\n            className=\"relative flex cursor-grab touch-none items-center gap-2 overflow-hidden p-4 outline-none select-none shadow-[0_1px_2px_rgba(0,0,0,0.05),0_4px_12px_rgba(0,0,0,0.1)] [-webkit-tap-highlight-color:transparent] [-webkit-touch-callout:none] [background:var(--st-bg)] [border-radius:var(--st-radius)] data-[swiping]:cursor-grabbing group-data-[dismissible=false]:cursor-default group-data-[dismissible=false]:touch-auto\"\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 as Node | null)) {\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\n                className=\"inline-flex h-[18px] w-[18px] flex-none [&_svg]:h-full [&_svg]:w-full\"\n                aria-hidden=\"true\"\n              >\n                {icon}\n              </span>\n            ) : null}\n            <span className=\"flex min-w-0 flex-auto flex-col gap-px\">\n              <span className=\"leading-normal font-medium\">{title}</span>\n              {description ? (\n                <span className=\"leading-[1.4] font-normal [color:color-mix(in_srgb,var(--st-ink)_66%,transparent)]\">\n                  {description}\n                </span>\n              ) : null}\n            </span>\n            {actionLabel ? (\n              <button\n                type=\"button\"\n                className=\"h-6 flex-none cursor-pointer touch-manipulation rounded-[5px] border-0 px-2 text-xs font-medium outline-none [-webkit-tap-highlight-color:transparent] [background:var(--st-ink)] [color:var(--st-bg)] [font-family:inherit] [transition:transform_160ms_cubic-bezier(0.23,1,0.32,1),opacity_160ms_ease] active:[transform:scale(0.97)] motion-reduce:active:[transform:none] [@media(hover:hover)_and_(pointer:fine)]:hover:opacity-[0.88]\"\n                onClick={() => {\n                  latest.current.onAction?.();\n                  close('action');\n                }}\n              >\n                {actionLabel}\n              </button>\n            ) : null}\n            {closeButton ? (\n              <button\n                type=\"button\"\n                className=\"relative grid h-6 w-6 flex-none cursor-pointer touch-manipulation place-items-center rounded-[5px] border-0 bg-transparent text-inherit outline-none [-webkit-tap-highlight-color:transparent] [font-family:inherit] [transition:transform_160ms_cubic-bezier(0.23,1,0.32,1),background-color_160ms_ease] before:absolute before:-inset-2 before:content-[''] active:[transform:scale(0.97)] motion-reduce:active:[transform:none] [@media(hover:hover)_and_(pointer:fine)]:hover:[background:color-mix(in_srgb,var(--st-ink)_10%,transparent)]\"\n                aria-label=\"Close\"\n                onClick={() => close('close')}\n              >\n                <HugeiconsIcon icon={Cancel01Icon} size={12} strokeWidth={2.5} />\n              </button>\n            ) : null}\n            <i\n              ref={fuseRef}\n              className=\"pointer-events-none absolute right-0 bottom-0 left-0 h-0.5 origin-left [background:linear-gradient(90deg,transparent_0%,color-mix(in_srgb,var(--st-fuse)_18%,transparent)_9%,color-mix(in_srgb,var(--st-fuse)_50%,transparent)_18%,color-mix(in_srgb,var(--st-fuse)_84%,transparent)_28%,var(--st-fuse)_38%,var(--st-fuse)_62%,color-mix(in_srgb,var(--st-fuse)_84%,transparent)_72%,color-mix(in_srgb,var(--st-fuse)_50%,transparent)_82%,color-mix(in_srgb,var(--st-fuse)_18%,transparent)_91%,transparent_100%)] group-data-[fuse=top]:top-0 group-data-[fuse=top]:bottom-auto group-data-[fuse=none]:opacity-0\"\n              aria-hidden=\"true\"\n            />\n          </motion.div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default SwipeToast;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12",
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}