Repository Public
1'use client';2import * as React from 'react';3import { animate, motion, useMotionValue } from 'motion/react';4import { cn } from '@/lib/cn';5import { haptic } from '@/lib/haptic';67export interface SwipeAction {8 label: string;9 icon: React.ReactNode;10 tone?: 'neutral' | 'accent' | 'danger';11 onAction: () => void;12}13const TONE = { neutral: 'bg-surface-2 text-text', accent: 'bg-accent text-accent-fg', danger: 'bg-error text-bg' } as const;14const W = 72;1516/**17 * Row that slides left to reveal actions (touch). The same actions must also be reachable18 * without the gesture (menus) — this is an accelerator, not the only path.19 */20export function SwipeRow({ actions, children, className }: { actions: SwipeAction[]; children: React.ReactNode; className?: string }) {21 const x = useMotionValue(0);22 const width = actions.length * W;23 const [open, setOpen] = React.useState(false);24 const snap = (to: number) => {25 setOpen(to !== 0);26 void animate(x, to, { type: 'spring', stiffness: 520, damping: 42 });27 };28 if (actions.length === 0) return <div className={className}>{children}</div>;29 return (30 <div className={cn('relative overflow-hidden', className)}>31 <div className="absolute inset-y-0 right-0 flex" style={{ width }} aria-hidden={!open}>32 {actions.map((a) => (33 <button34 key={a.label}35 tabIndex={open ? 0 : -1}36 onClick={() => {37 haptic(12);38 snap(0);39 a.onAction();40 }}41 className={cn('flex h-full flex-col items-center justify-center gap-1 text-[11px] font-medium', TONE[a.tone ?? 'neutral'])}42 style={{ width: W }}43 >44 {a.icon}45 {a.label}46 </button>47 ))}48 </div>49 <motion.div50 style={{ x }}51 drag="x"52 dragDirectionLock53 dragConstraints={{ left: -width, right: 0 }}54 dragElastic={{ left: 0.15, right: 0 }}55 onDragEnd={(_, info) => snap(info.offset.x < -width / 3 || info.velocity.x < -400 ? -width : 0)}56 onClickCapture={(e) => {57 if (open) {58 e.preventDefault();59 e.stopPropagation();60 snap(0);61 }62 }}63 className="relative touch-pan-y bg-surface"64 >65 {children}66 </motion.div>67 </div>68 );69}70