{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bottom-sheet",
  "title": "Bottom Sheet",
  "description": "A modal sheet that rises from the bottom edge, rests at snap points you choose, drags and flings between them, and closes with Escape, the overlay or a downward drag.",
  "dependencies": [
    "@radix-ui/react-dialog",
    "@use-gesture/react"
  ],
  "files": [
    {
      "path": "registry/tealmac/bottom-sheet.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { useDrag } from \"@use-gesture/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* -------------------------------------------------------------------------- */\n/* Snap math, shared with PersistentSheet                                      */\n/* -------------------------------------------------------------------------- */\n\n/** Snap points are fractions of the viewport height, for example `[0.4, 1]`. */\nexport type SnapPoints = number[]\n\nfunction clamp(n: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, n))\n}\n\n/** A short spoken name for a snap point: \"Half height\", \"Full height\". */\nexport function snapLabel(fraction: number) {\n  if (fraction >= 0.98) return \"Full height\"\n  return `${Math.round(fraction * 100)}% of the screen`\n}\n\ninterface SnapDragOptions {\n  /** Sorted ascending, fractions of the viewport height. */\n  snapPoints: SnapPoints\n  /** Index into `snapPoints`. */\n  snap: number\n  onSnapChange: (index: number) => void\n  /** Called when a drag ends below the lowest snap point. Omit to forbid it. */\n  onDismiss?: () => void\n  /** Element whose height is the viewport reference (the sheet itself). */\n  panelRef: React.RefObject<HTMLElement | null>\n}\n\n/**\n * Pointer drag for a sheet that sits on the bottom edge. It returns the live\n * offset in pixels, whether a drag is running and `bind` for the grab areas\n * (the handle and header). A fling is projected forward before the nearest\n * snap point is chosen.\n */\nexport function useSnapDrag({\n  snapPoints,\n  snap,\n  onSnapChange,\n  onDismiss,\n  panelRef,\n}: SnapDragOptions) {\n  const [dragY, setDragY] = React.useState(0)\n  const [dragging, setDragging] = React.useState(false)\n  const max = snapPoints[snapPoints.length - 1]\n\n  const bind = useDrag(\n    ({\n      first,\n      last,\n      movement: [, my],\n      velocity: [, vy],\n      direction: [, dy],\n      event,\n    }) => {\n      const panel = panelRef.current\n      // Arrow keys on the handle are handled by SnapHandle, not as a drag.\n      if (!panel || event.type.startsWith(\"key\")) return\n      const viewport = panel.offsetHeight / max\n      if (first) setDragging(true)\n      // Do not drag above the top snap point.\n      const top = (max - snapPoints[snap]) * viewport\n      const next = Math.max(-top, my)\n      if (!last) {\n        setDragY(next)\n        return\n      }\n      setDragging(false)\n      setDragY(0)\n      const visible = snapPoints[snap] * viewport - next - vy * dy * 120\n      const lowest = snapPoints[0] * viewport\n      if (onDismiss && visible < lowest * 0.55) {\n        onDismiss()\n        return\n      }\n      let best = 0\n      let bestDistance = Infinity\n      snapPoints.forEach((fraction, index) => {\n        const distance = Math.abs(fraction * viewport - visible)\n        if (distance < bestDistance) {\n          best = index\n          bestDistance = distance\n        }\n      })\n      onSnapChange(best)\n    },\n    {\n      axis: \"y\",\n      threshold: 4,\n      pointer: { touch: true, capture: false },\n      // Pull the grab areas with the finger, never scroll the page.\n      eventOptions: { passive: false },\n    }\n  )\n\n  return { bind, dragY, dragging }\n}\n\ninterface SnapHandleProps extends React.ComponentProps<\"div\"> {\n  snapPoints: SnapPoints\n  snap: number\n  onSnapChange: (index: number) => void\n  label?: string\n}\n\n/**\n * The grab handle. It is a focusable `separator` (the WAI-ARIA window splitter\n * pattern): Up and Down move between snap points, Home and End jump to the\n * lowest and the highest.\n */\nexport function SnapHandle({\n  snapPoints,\n  snap,\n  onSnapChange,\n  label = \"Resize sheet\",\n  className,\n  onKeyDown,\n  ...props\n}: SnapHandleProps) {\n  const last = snapPoints.length - 1\n  const fraction = snapPoints[snap]\n  return (\n    // A focusable separator is an interactive widget (WAI-ARIA window splitter).\n    // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n    <div\n      data-slot=\"sheet-handle\"\n      role=\"separator\"\n      aria-orientation=\"horizontal\"\n      aria-label={label}\n      aria-valuemin={Math.round(snapPoints[0] * 100)}\n      aria-valuemax={Math.round(snapPoints[last] * 100)}\n      aria-valuenow={Math.round(fraction * 100)}\n      aria-valuetext={snapLabel(fraction)}\n      tabIndex={last > 0 ? 0 : -1}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (event.defaultPrevented || last === 0) return\n        const key = event.key\n        if (key === \"ArrowUp\") onSnapChange(clamp(snap + 1, 0, last))\n        else if (key === \"ArrowDown\") onSnapChange(clamp(snap - 1, 0, last))\n        else if (key === \"Home\") onSnapChange(0)\n        else if (key === \"End\") onSnapChange(last)\n        else return\n        event.preventDefault()\n      }}\n      className={cn(\n        \"group/handle flex min-h-8 touch-none items-center justify-center rounded-t-[inherit] py-2 outline-none select-none\",\n        className\n      )}\n      {...props}\n    >\n      <span\n        aria-hidden\n        className=\"bg-muted-foreground/40 group-focus-visible/handle:bg-ring h-1.5 w-10 rounded-full transition-colors\"\n      />\n    </div>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* The sheet                                                                   */\n/* -------------------------------------------------------------------------- */\n\ninterface SheetContextValue {\n  snapPoints: SnapPoints | undefined\n  snap: number\n  setSnap: (index: number) => void\n  dismissible: boolean\n  close: () => void\n}\n\nconst SheetContext = React.createContext<SheetContextValue | null>(null)\n\nfunction useSheet() {\n  const ctx = React.useContext(SheetContext)\n  if (!ctx) throw new Error(\"Use BottomSheet parts inside <BottomSheet>.\")\n  return ctx\n}\n\nexport interface BottomSheetProps extends React.ComponentProps<\n  typeof DialogPrimitive.Root\n> {\n  /**\n   * Heights the sheet can rest at, as fractions of the screen, ascending:\n   * `[0.4, 0.8, 1]`. Leave it out for a sheet that is as tall as its content.\n   */\n  snapPoints?: SnapPoints\n  /** Controlled snap index. */\n  snap?: number\n  /** Starting snap index. Default: the lowest. */\n  defaultSnap?: number\n  onSnapChange?: (index: number) => void\n  /** Allow dragging down, Escape and the overlay to close. Default true. */\n  dismissible?: boolean\n}\n\nfunction BottomSheet({\n  snapPoints,\n  snap: snapProp,\n  defaultSnap = 0,\n  onSnapChange,\n  dismissible = true,\n  open: openProp,\n  defaultOpen,\n  onOpenChange,\n  children,\n  ...props\n}: BottomSheetProps) {\n  const [snapState, setSnapState] = React.useState(defaultSnap)\n  const [openState, setOpenState] = React.useState(defaultOpen ?? false)\n  const open = openProp ?? openState\n  const snap = clamp(snapProp ?? snapState, 0, (snapPoints?.length ?? 1) - 1)\n\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (!next && !dismissible) return\n      setOpenState(next)\n      onOpenChange?.(next)\n    },\n    [dismissible, onOpenChange]\n  )\n  const setSnap = React.useCallback(\n    (index: number) => {\n      setSnapState(index)\n      onSnapChange?.(index)\n    },\n    [onSnapChange]\n  )\n  const value = React.useMemo(\n    () => ({\n      snapPoints,\n      snap,\n      setSnap,\n      dismissible,\n      close: () => setOpen(false),\n    }),\n    [snapPoints, snap, setSnap, dismissible, setOpen]\n  )\n\n  return (\n    <SheetContext.Provider value={value}>\n      <DialogPrimitive.Root\n        data-slot=\"bottom-sheet\"\n        open={open}\n        onOpenChange={setOpen}\n        {...props}\n      >\n        {children}\n      </DialogPrimitive.Root>\n    </SheetContext.Provider>\n  )\n}\n\nfunction BottomSheetTrigger(\n  props: React.ComponentProps<typeof DialogPrimitive.Trigger>\n) {\n  return <DialogPrimitive.Trigger data-slot=\"bottom-sheet-trigger\" {...props} />\n}\n\nfunction BottomSheetClose(\n  props: React.ComponentProps<typeof DialogPrimitive.Close>\n) {\n  return <DialogPrimitive.Close data-slot=\"bottom-sheet-close\" {...props} />\n}\n\nexport interface BottomSheetContentProps extends Omit<\n  React.ComponentProps<typeof DialogPrimitive.Content>,\n  \"children\"\n> {\n  children?: React.ReactNode\n  /** Accessible name of the drag handle. */\n  handleLabel?: string\n  /** Hide the drag handle (the sheet can still be closed with Escape). */\n  hideHandle?: boolean\n}\n\nfunction BottomSheetContent({\n  className,\n  children,\n  style,\n  handleLabel,\n  hideHandle,\n  onEscapeKeyDown,\n  onInteractOutside,\n  ...props\n}: BottomSheetContentProps) {\n  const { snapPoints, snap, setSnap, dismissible, close } = useSheet()\n  const panelRef = React.useRef<HTMLDivElement>(null)\n  const points = snapPoints ?? [1]\n  const max = points[points.length - 1]\n  const fit = !snapPoints\n  const multi = points.length > 1\n  const atTop = snap === points.length - 1\n\n  const { bind, dragY, dragging } = useSnapDrag({\n    snapPoints: points,\n    snap,\n    onSnapChange: setSnap,\n    onDismiss: dismissible ? close : undefined,\n    panelRef,\n  })\n\n  // A fit-content sheet only moves down.\n  const offset = fit ? Math.max(0, dragY) : dragY\n  const rest = fit ? \"0px\" : `${(max - points[snap]) * 100}dvh`\n\n  return (\n    <DialogPrimitive.Portal>\n      <DialogPrimitive.Overlay\n        data-slot=\"bottom-sheet-overlay\"\n        className=\"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 motion-reduce:animate-none\"\n      />\n      <DialogPrimitive.Content\n        ref={panelRef}\n        data-slot=\"bottom-sheet-content\"\n        data-snap={snap}\n        data-dragging={dragging || undefined}\n        onEscapeKeyDown={(event) => {\n          onEscapeKeyDown?.(event)\n          if (!dismissible) event.preventDefault()\n        }}\n        onInteractOutside={(event) => {\n          onInteractOutside?.(event)\n          if (!dismissible) event.preventDefault()\n        }}\n        style={{\n          ...(fit ? { maxHeight: \"92dvh\" } : { height: `${max * 100}dvh` }),\n          transform: `translateY(calc(${rest} + ${offset}px))`,\n          ...style,\n        }}\n        className={cn(\n          \"bg-background fixed inset-x-0 bottom-0 z-50 mx-auto flex w-full max-w-2xl flex-col rounded-t-2xl border border-b-0 shadow-xl outline-none\",\n          \"data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom data-[state=open]:animate-in data-[state=open]:slide-in-from-bottom duration-300 motion-reduce:animate-none\",\n          !dragging &&\n            \"transition-transform duration-300 ease-out motion-reduce:transition-none\",\n          className\n        )}\n        {...props}\n      >\n        {!hideHandle && (\n          <div {...bind()} role=\"presentation\" className=\"touch-none\">\n            <SnapHandle\n              snapPoints={points}\n              snap={snap}\n              onSnapChange={setSnap}\n              label={handleLabel}\n            />\n          </div>\n        )}\n        <div\n          data-slot=\"bottom-sheet-body\"\n          // Only the tallest snap point scrolls. Lower ones are dragged.\n          className={cn(\n            \"flex min-h-0 flex-1 flex-col overscroll-contain pb-[env(safe-area-inset-bottom)]\",\n            fit || atTop || !multi\n              ? \"overflow-y-auto\"\n              : \"touch-none overflow-hidden\"\n          )}\n          {...(fit || atTop || !multi ? {} : bind())}\n        >\n          {children}\n        </div>\n      </DialogPrimitive.Content>\n    </DialogPrimitive.Portal>\n  )\n}\n\nfunction BottomSheetHeader({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"bottom-sheet-header\"\n      className={cn(\"flex flex-col gap-1 px-4 pb-3 text-start\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction BottomSheetFooter({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"bottom-sheet-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction BottomSheetTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"bottom-sheet-title\"\n      className={cn(\"text-lg leading-tight font-semibold\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction BottomSheetDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"bottom-sheet-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  BottomSheet,\n  BottomSheetClose,\n  BottomSheetContent,\n  BottomSheetDescription,\n  BottomSheetFooter,\n  BottomSheetHeader,\n  BottomSheetTitle,\n  BottomSheetTrigger,\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}