{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "SwipeRow-JS-CSS",
	"title": "SwipeRow",
	"description": "List row that swipes open to reveal actions and deletes on a full swipe.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "SwipeRow.css",
			"target": "@components/SwipeRow.css",
			"content": ".swipe-row {\n  --sr-h: 64px;\n  --sr-r: 16px;\n  --sr-a: 80px;\n  --sr-row: #27272a;\n  --sr-text: #f5f5f5;\n  --sr-drawer: #3f3f46;\n  --sr-on-drawer: #ffffff;\n  --sr-action: #e5484d;\n  --sr-on-action: #ffffff;\n  --sr-collapse: 200ms;\n  --sr-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n\n  position: relative;\n  height: var(--sr-h);\n  overflow: hidden;\n  opacity: 1;\n  transition:\n    height var(--sr-collapse) var(--sr-ease-out),\n    margin-bottom var(--sr-collapse) var(--sr-ease-out),\n    opacity var(--sr-collapse) var(--sr-ease-out);\n}\n\n.swipe-row[data-phase='collapsing'] {\n  height: 0 !important;\n  margin-bottom: 0 !important;\n  opacity: 0;\n}\n\n.swipe-row[data-disabled] {\n  opacity: 0.55;\n  pointer-events: none;\n}\n\n.swipe-row__clip {\n  position: relative;\n  height: var(--sr-h);\n  border-radius: var(--sr-r);\n  overflow: hidden;\n  background: var(--sr-row);\n}\n\n.swipe-row__rail {\n  position: absolute;\n  inset: 0;\n  background: var(--sr-drawer);\n}\n\n.swipe-row__action {\n  position: absolute;\n  top: 0;\n  display: grid;\n  place-items: center;\n  width: var(--sr-a);\n  height: 100%;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  background: var(--sr-drawer);\n  color: var(--sr-on-drawer);\n  font: inherit;\n  cursor: pointer;\n  outline: none;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-touch-callout: none;\n  -webkit-tap-highlight-color: transparent;\n  touch-action: manipulation;\n}\n\n.swipe-row__action::after {\n  content: '';\n  position: absolute;\n  inset: 0;\n  background: #fff;\n  opacity: 0;\n  pointer-events: none;\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .swipe-row__action::after {\n    transition: opacity 150ms ease;\n  }\n\n  .swipe-row__action:hover::after {\n    opacity: 0.08;\n  }\n}\n\n.swipe-row__glyph {\n  display: grid;\n  justify-items: center;\n  gap: 4px;\n  font-size: 11px;\n  font-weight: 500;\n  letter-spacing: 0.01em;\n  line-height: 1;\n  transition: transform 160ms var(--sr-ease-out);\n}\n\n.swipe-row__icon {\n  display: inline-flex;\n}\n\n.swipe-row__action:active .swipe-row__glyph {\n  transform: scale(0.97);\n}\n\n.swipe-row__block {\n  position: absolute;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  background: var(--sr-action);\n}\n\n.swipe-row[data-direction='left'] .swipe-row__block {\n  left: calc(100% - var(--sr-a));\n}\n\n.swipe-row[data-direction='right'] .swipe-row__block {\n  right: calc(100% - var(--sr-a));\n}\n\n.swipe-row__action--commit {\n  background: transparent;\n  color: var(--sr-on-action);\n}\n\n.swipe-row[data-direction='left'] .swipe-row__action--commit {\n  left: 0;\n}\n\n.swipe-row[data-direction='right'] .swipe-row__action--commit {\n  right: 0;\n}\n\n.swipe-row__surface {\n  position: relative;\n  z-index: 1;\n  display: flex;\n  align-items: center;\n  gap: 12px;\n  height: 100%;\n  padding: 0 16px;\n  background: var(--sr-row);\n  color: var(--sr-text);\n  touch-action: pan-y;\n  -webkit-touch-callout: none;\n  -webkit-tap-highlight-color: transparent;\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .swipe-row__surface {\n    cursor: grab;\n  }\n\n  .swipe-row[data-dragging] .swipe-row__surface {\n    cursor: grabbing;\n  }\n}\n\n@media (pointer: coarse) {\n  .swipe-row__surface {\n    user-select: none;\n    -webkit-user-select: none;\n  }\n}\n\n.swipe-row[data-dragging] .swipe-row__surface,\n.swipe-row[data-dragging] .swipe-row__surface * {\n  user-select: none;\n  -webkit-user-select: none;\n}\n\n.swipe-row__toggle {\n  position: absolute;\n  top: 50%;\n  width: 1px;\n  height: 1px;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  overflow: hidden;\n  clip-path: inset(50%);\n  background: transparent;\n  color: var(--sr-text);\n  font: inherit;\n  outline: none;\n}\n\n.swipe-row[data-direction='left'] .swipe-row__toggle {\n  right: 12px;\n}\n\n.swipe-row[data-direction='right'] .swipe-row__toggle {\n  left: 12px;\n}\n\n.swipe-row__toggle:focus-visible {\n  width: auto;\n  height: 24px;\n  padding: 0 10px;\n  border-radius: 12px;\n  overflow: visible;\n  clip-path: none;\n  background: color-mix(in srgb, var(--sr-text) 12%, transparent);\n  font-size: 12px;\n  white-space: nowrap;\n  transform: translateY(-50%);\n}\n\n.swipe-row__sr {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  overflow: hidden;\n  clip-path: inset(50%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .swipe-row__action:active .swipe-row__glyph {\n    transform: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "SwipeRow.jsx",
			"content": "'use client';\n\nimport { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';\nimport { animate, motion, useMotionValue, useReducedMotion, useTransform } from 'motion/react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { Delete02Icon } from '@hugeicons/core-free-icons';\n\nimport './SwipeRow.css';\n\nconst HYST = 10;\nconst FLICK = 110;\nconst DECEL = 0.998;\nconst VMAX = 1500;\nconst EASE_OUT = [0.23, 1, 0.32, 1];\nconst SPRING_UI = { type: 'spring', duration: 0.3, bounce: 0 };\nconst DEFAULT_ACTIONS = [{ id: 'delete', label: 'Delete' }];\n\nconst clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));\nconst rubber = (o, dim, c) => (o * dim * c) / (dim + c * Math.abs(o));\nconst unrubber = (y, dim, c) => (y * dim) / (c * Math.max(1, dim - Math.abs(y)));\nconst project = v => ((v / 1000) * DECEL) / (1 - DECEL);\nconst velocityOf = hist => {\n  if (hist.length < 2) return 0;\n  const a = hist[0];\n  const b = hist[hist.length - 1];\n  return ((b[1] - a[1]) / Math.max(1, b[0] - a[0])) * 1000;\n};\nconst onColor = hex => {\n  const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());\n  if (!m) return '#ffffff';\n  const h = m[1].length === 3 ? [...m[1]].map(ch => ch + ch).join('') : m[1];\n  const r = parseInt(h.slice(0, 2), 16);\n  const g = parseInt(h.slice(2, 4), 16);\n  const b = parseInt(h.slice(4, 6), 16);\n  return (r * 299 + g * 587 + b * 114) / 1000 >= 150 ? '#111111' : '#ffffff';\n};\nconst watchWindow = live => {\n  const onMove = e => {\n    if (e.isTrusted) live.current.move(e);\n  };\n  const onUp = e => {\n    if (e.isTrusted) live.current.up(e);\n  };\n  window.addEventListener('pointermove', onMove);\n  window.addEventListener('pointerup', onUp);\n  window.addEventListener('pointercancel', onUp);\n  return () => {\n    window.removeEventListener('pointermove', onMove);\n    window.removeEventListener('pointerup', onUp);\n    window.removeEventListener('pointercancel', onUp);\n  };\n};\n\nexport default function SwipeRow({\n  children,\n  actions = DEFAULT_ACTIONS,\n  actionColor = '#e5484d',\n  drawerColor = '#3f3f46',\n  rowColor = '#27272a',\n  textColor = '#f5f5f5',\n  height = 64,\n  radius = 16,\n  actionWidth = 80,\n  direction = 'left',\n  snapBounce = 0.2,\n  resistance = 0.55,\n  collapseMs = 200,\n  commitAt = 0.6,\n  fullSwipe = true,\n  disabled = false,\n  open: openProp,\n  onOpenChange,\n  onAction,\n  onCommit,\n  closeOnAction = true,\n  haptic = true,\n  label = 'List item',\n  className = '',\n  style\n}) {\n  const uid = useId();\n  const reduce = useReducedMotion();\n  const s = direction === 'left' ? -1 : 1;\n  const A = actionWidth;\n  const n = actions.length;\n  const D = n * A;\n  const c = clamp(resistance, 0.05, 1);\n  const primary = actions[0];\n  const [openState, setOpenState] = useState(false);\n  const [phase, setPhase] = useState('idle');\n  const [say, setSay] = useState('');\n  const open = openProp ?? openState;\n\n  const root = useRef(null);\n  const surface = useRef(null);\n  const w = useRef(360);\n  const grip = useRef(null);\n  const unwatch = useRef(null);\n  const live = useRef({});\n  const foldTimer = useRef(undefined);\n  const heading = useRef(null);\n\n  const x = useMotionValue(0);\n  const spread = useMotionValue(0);\n  const landed = useMotionValue(0);\n  const commitPoint = () => Math.max(commitAt * w.current, D + A / 2);\n  const canCommit = () => fullSwipe && n > 0 && commitPoint() <= w.current;\n  const exposed = useTransform(x, v => s * v);\n  const surfaceXf = useTransform(x, v => `translateX(${v}px)`);\n  const railXf = useTransform(exposed, e => `translateX(${-s * Math.max(0, D - e)}px)`);\n  const shift = useTransform([exposed, spread], ([e, p]) => p * Math.max(0, e - A));\n  const blockXf = useTransform(shift, v => `translateX(${s * v}px)`);\n  const glyphXf = useTransform([shift, landed], ([v, l]) => `translateX(${-s * l * (v - (w.current - A) / 2)}px)`);\n\n  const map = raw => {\n    const W = w.current;\n    if (raw < 0) return rubber(raw, W, c);\n    if (raw <= D) return raw;\n    if (!canCommit()) return D + rubber(raw - D, W, c);\n    const C = commitPoint();\n    const knee = D + (C - D) / c;\n    return raw <= knee ? D + c * (raw - D) : C + rubber(raw - knee, W, c);\n  };\n  const inv = ex => {\n    const W = w.current;\n    if (ex < 0) return unrubber(ex, W, c);\n    if (ex <= D) return ex;\n    if (!canCommit()) return D + unrubber(ex - D, W, c);\n    const C = commitPoint();\n    const knee = D + (C - D) / c;\n    return ex <= C ? D + (ex - D) / c : knee + unrubber(ex - C, W, c);\n  };\n\n  useLayoutEffect(() => {\n    const el = root.current;\n    if (!el) return undefined;\n    w.current = el.offsetWidth || w.current;\n    const observer = new ResizeObserver(entries => {\n      const width = entries[0]?.contentRect.width;\n      if (width) w.current = width;\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, []);\n  useEffect(\n    () => () => {\n      clearTimeout(foldTimer.current);\n      unwatch.current?.();\n    },\n    []\n  );\n\n  const setOpen = next => {\n    if (next === open) return;\n    setOpenState(next);\n    onOpenChange?.(next);\n  };\n  const settle = (target, v = 0) => {\n    heading.current = target;\n    if (reduce) {\n      animate(x, s * target, { duration: 0.2, ease: EASE_OUT });\n      return;\n    }\n    const flick = Math.abs(v) >= FLICK;\n    animate(\n      x,\n      s * target,\n      flick\n        ? { type: 'spring', duration: 0.4, bounce: snapBounce, velocity: s * clamp(v, -VMAX, VMAX) }\n        : { ...SPRING_UI, velocity: s * v }\n    );\n  };\n  const setSpread = on => {\n    if ((spread.get() === 1) === on) return;\n    if (reduce) spread.set(on ? 1 : 0);\n    else animate(spread, on ? 1 : 0, SPRING_UI);\n    if (on && primary) {\n      setSay(`Release to ${primary.label}`);\n      if (haptic && grip.current?.touch) navigator.vibrate?.(8);\n    }\n  };\n  const commit = (a, viaKey, v = 0) => {\n    const leap = a === primary;\n    setPhase('committing');\n    setSay(a.label);\n    setOpen(false);\n    const fold = () => {\n      setPhase('collapsing');\n      foldTimer.current = setTimeout(() => {\n        onCommit?.(a);\n        a.onSelect?.();\n      }, collapseMs);\n    };\n    if (viaKey || reduce) {\n      if (leap) {\n        spread.set(1);\n        landed.set(1);\n      }\n      if (viaKey) {\n        x.set(s * w.current);\n        fold();\n      } else animate(x, s * w.current, { duration: 0.2, ease: EASE_OUT }).then(fold);\n      return;\n    }\n    if (leap) {\n      if (spread.get() < 1) animate(spread, 1, SPRING_UI);\n      animate(landed, 1, SPRING_UI);\n    }\n    animate(x, s * w.current, { ...SPRING_UI, velocity: s * v }).then(fold);\n  };\n  useEffect(() => {\n    if (openProp === undefined || grip.current || phase !== 'idle') return;\n    const target = open ? D : 0;\n    if (heading.current === target) return;\n    if (Math.abs(exposed.get() - target) > 0.5) settle(target);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [open, D]);\n\n  const down = e => {\n    if (disabled || n === 0 || phase !== 'idle' || grip.current || e.button !== 0) return;\n    x.stop();\n    heading.current = null;\n    grip.current = {\n      id: e.pointerId,\n      x0: e.clientX,\n      y0: e.clientY,\n      grab: null,\n      moved: false,\n      hist: [],\n      touch: e.pointerType === 'touch'\n    };\n    try {\n      surface.current?.setPointerCapture(e.pointerId);\n    } catch {}\n    unwatch.current?.();\n    unwatch.current = watchWindow(live);\n  };\n  const move = e => {\n    const g = grip.current;\n    if (!g || g.id !== e.pointerId) return;\n    if (g.grab === null) {\n      const dx = e.clientX - g.x0;\n      const dy = e.clientY - g.y0;\n      if (Math.abs(dx) < HYST || Math.abs(dx) < Math.abs(dy)) return;\n      g.grab = s * (g.x0 + Math.sign(dx) * HYST) - inv(exposed.get());\n      g.moved = true;\n      root.current?.setAttribute('data-dragging', '');\n    }\n    const ex = map(s * e.clientX - g.grab);\n    x.set(s * ex);\n    g.hist.push([performance.now(), ex]);\n    if (g.hist.length > 4) g.hist.shift();\n    setSpread(canCommit() && ex >= commitPoint());\n  };\n  const up = e => {\n    const g = grip.current;\n    if (!g || g.id !== e.pointerId) return;\n    grip.current = null;\n    unwatch.current?.();\n    unwatch.current = null;\n    root.current?.removeAttribute('data-dragging');\n    try {\n      surface.current?.releasePointerCapture(e.pointerId);\n    } catch {}\n    const ex = exposed.get();\n    const v = velocityOf(g.hist);\n    if (!g.moved) {\n      if (open) {\n        setOpen(false);\n        settle(0);\n      }\n      return;\n    }\n    if (primary && canCommit() && ex >= commitPoint()) {\n      commit(primary, false, v);\n      return;\n    }\n    const target = Math.abs(v) >= FLICK ? (v > 0 ? D : 0) : ex + project(v) > D / 2 ? D : 0;\n    setSpread(false);\n    setOpen(target === D);\n    settle(target, v);\n  };\n  live.current = { move, up };\n\n  const act = (a, e) => {\n    if (phase !== 'idle') return;\n    onAction?.(a);\n    if (a === primary || a.dismiss) {\n      commit(a, e.detail === 0);\n      return;\n    }\n    a.onSelect?.();\n    if (!closeOnAction) return;\n    setOpen(false);\n    if (e.detail === 0) {\n      heading.current = 0;\n      x.set(0);\n    } else settle(0);\n  };\n  const openNow = () => {\n    heading.current = D;\n    x.set(s * D);\n    setOpen(true);\n    setSay(`${n} actions revealed`);\n  };\n  const closeNow = () => {\n    heading.current = 0;\n    x.set(0);\n    setOpen(false);\n  };\n  const onToggleKey = e => {\n    if (disabled || phase !== 'idle' || n === 0) return;\n    const openKey = s < 0 ? 'ArrowLeft' : 'ArrowRight';\n    const closeKey = s < 0 ? 'ArrowRight' : 'ArrowLeft';\n    const toggle = e.key === 'Enter' || e.key === ' ';\n    if (e.key === openKey || (toggle && !open)) {\n      e.preventDefault();\n      openNow();\n    } else if (e.key === closeKey || e.key === 'Escape' || (toggle && open)) {\n      e.preventDefault();\n      closeNow();\n    } else if ((e.key === 'Delete' || e.key === 'Backspace') && open && primary && canCommit()) {\n      e.preventDefault();\n      commit(primary, true);\n    }\n  };\n  const onToggleClick = e => {\n    if (e.detail !== 0 || disabled || phase !== 'idle' || n === 0) return;\n    if (open) closeNow();\n    else openNow();\n  };\n\n  const railId = `${uid}-rail`;\n  return (\n    <div\n      ref={root}\n      role=\"group\"\n      aria-label={label}\n      className={`swipe-row${className ? ` ${className}` : ''}`}\n      data-direction={direction}\n      data-open={open ? '' : undefined}\n      data-phase={phase}\n      data-disabled={disabled ? '' : undefined}\n      style={{\n        '--sr-h': `${height}px`,\n        '--sr-r': `${radius}px`,\n        '--sr-a': `${A}px`,\n        '--sr-row': rowColor,\n        '--sr-text': textColor,\n        '--sr-drawer': drawerColor,\n        '--sr-on-drawer': onColor(drawerColor),\n        '--sr-action': actionColor,\n        '--sr-on-action': onColor(actionColor),\n        '--sr-collapse': `${collapseMs}ms`,\n        ...style\n      }}\n    >\n      <div className=\"swipe-row__clip\">\n        <motion.div\n          id={railId}\n          className=\"swipe-row__rail\"\n          style={{ transform: railXf }}\n          inert={!open || undefined}\n          aria-hidden={!open}\n        >\n          {actions.slice(1).map((a, i) => (\n            <button\n              key={a.id}\n              type=\"button\"\n              className=\"swipe-row__action\"\n              onClick={e => act(a, e)}\n              style={\n                s < 0\n                  ? { right: (i + 1) * A, background: a.color ?? drawerColor, color: onColor(a.color ?? drawerColor) }\n                  : { left: (i + 1) * A, background: a.color ?? drawerColor, color: onColor(a.color ?? drawerColor) }\n              }\n            >\n              <span className=\"swipe-row__glyph\">\n                {a.icon ? <span className=\"swipe-row__icon\">{a.icon}</span> : null}\n                <span>{a.label}</span>\n              </span>\n            </button>\n          ))}\n          {primary ? (\n            <motion.div className=\"swipe-row__block\" style={{ transform: blockXf }}>\n              <motion.button\n                type=\"button\"\n                className=\"swipe-row__action swipe-row__action--commit\"\n                style={{ transform: glyphXf }}\n                onClick={e => act(primary, e)}\n              >\n                <span className=\"swipe-row__glyph\">\n                  <span className=\"swipe-row__icon\">\n                    {primary.icon ?? <HugeiconsIcon icon={Delete02Icon} size={20} strokeWidth={2} />}\n                  </span>\n                  <span>{primary.label}</span>\n                </span>\n              </motion.button>\n            </motion.div>\n          ) : null}\n        </motion.div>\n        <motion.div ref={surface} className=\"swipe-row__surface\" style={{ transform: surfaceXf }} onPointerDown={down}>\n          {children}\n          <button\n            type=\"button\"\n            className=\"swipe-row__toggle\"\n            tabIndex={disabled ? -1 : 0}\n            aria-expanded={open}\n            aria-controls={railId}\n            aria-keyshortcuts={s < 0 ? 'ArrowLeft' : 'ArrowRight'}\n            onKeyDown={onToggleKey}\n            onClick={onToggleClick}\n          >\n            {n} {n === 1 ? 'action' : 'actions'}\n          </button>\n        </motion.div>\n      </div>\n      <span className=\"swipe-row__sr\" aria-live=\"polite\">\n        {say}\n      </span>\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12",
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}