SPB Git

Repository Public

swipe-row.tsx 2.3 KB 70 lines tsx

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
Files main
Menu

Theme