SPB Git

Repository Public

bottom-sheet.tsx 2.8 KB 65 lines tsx

1'use client';2import * as React from 'react';3import * as D from '@radix-ui/react-dialog';4import { AnimatePresence, motion, useDragControls } from 'motion/react';5import { cn } from '@/lib/cn';67/**8 * Phone sheet that follows the finger: drag the handle/header down to dismiss (distance or9 * velocity), springs back otherwise. Focus-trapped and labelled through Radix Dialog.10 */11export function BottomSheet({12  open, onOpenChange, title, children, className, headerExtra, testId,13}: {14  open: boolean;15  onOpenChange: (open: boolean) => void;16  title: string;17  children: React.ReactNode;18  className?: string;19  headerExtra?: React.ReactNode;20  testId?: string;21}) {22  const controls = useDragControls();23  return (24    <D.Root open={open} onOpenChange={onOpenChange}>25      <AnimatePresence>26        {open && (27          <D.Portal forceMount>28            <D.Overlay asChild forceMount>29              <motion.div className="fixed inset-0 z-50 bg-scrim" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.18 }} />30            </D.Overlay>31            <D.Content asChild forceMount onOpenAutoFocus={(e) => e.preventDefault()}>32              <motion.div33                data-testid={testId}34                className={cn('fixed inset-x-0 bottom-0 z-50 mx-auto flex max-h-[88dvh] w-full max-w-xl flex-col rounded-t-[26px] bg-surface shadow-pop focus:outline-none', className)}35                initial={{ y: '100%' }}36                animate={{ y: 0 }}37                exit={{ y: '100%' }}38                transition={{ type: 'spring', stiffness: 420, damping: 38, mass: 0.9 }}39                drag="y"40                dragControls={controls}41                dragListener={false}42                dragConstraints={{ top: 0, bottom: 0 }}43                dragElastic={{ top: 0, bottom: 0.7 }}44                onDragEnd={(_, info) => {45                  if (info.offset.y > 110 || info.velocity.y > 650) onOpenChange(false);46                }}47              >48                <div className="shrink-0 cursor-grab touch-none active:cursor-grabbing" onPointerDown={(e) => controls.start(e)}>49                  <div className="mx-auto mt-2.5 h-1.5 w-10 rounded-full bg-line-3" aria-hidden />50                  <div className="flex items-center gap-2 px-5 pb-2 pt-3">51                    <D.Title className="min-w-0 flex-1 truncate text-[17px] font-semibold tracking-tight">{title}</D.Title>52                    <D.Description className="sr-only">{title}</D.Description>53                    {headerExtra}54                  </div>55                </div>56                <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain pb-[max(16px,env(safe-area-inset-bottom))]">{children}</div>57              </motion.div>58            </D.Content>59          </D.Portal>60        )}61      </AnimatePresence>62    </D.Root>63  );64}65
Files main
Menu

Theme