{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "sheet", "type": "registry:ui", "title": "Sheet", "description": "Bottom sheet via RN Modal slide.", "registryDependencies": [ "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/utils.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/text.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/theme.json" ], "files": [ { "path": "registry/shared/ui/sheet.tsx", "type": "registry:ui", "target": "components/ui/sheet.tsx", "content": "import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport {\n Animated,\n Modal,\n PanResponder,\n type PanResponderGestureState,\n Pressable,\n useWindowDimensions,\n View,\n type ViewProps,\n} from 'react-native';\nimport { Text, type TextProps } from '@/components/ui/text';\nimport { cn } from '@/lib/utils';\n\n/** Snap point: fraction (0–1), px number, or percent string like '50%'. */\nexport type SnapPoint = number | `${number}%`;\n\ninterface SheetContextValue {\n /** Current translateY (bottom-anchored sheet). */\n translateY?: Animated.Value;\n /** Pan handlers attached to the drag-handle area. */\n panHandlers?: ViewProps;\n /** Effective sheet height in px when snapPoints are used. */\n sheetHeight?: number;\n}\n\nconst SheetContext = createContext({});\n\nexport interface SheetProps {\n open: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Render the dimmed backdrop. Tapping outside still closes when hidden. */\n showOverlay?: boolean;\n /**\n * Detent heights the sheet snaps to, e.g. ['30%', '60%'] or [240, 480].\n * When provided, the sheet drag-snaps between them; dragging past the\n * lowest point dismisses. Omit for auto-height (no drag).\n */\n snapPoints?: SnapPoint[];\n /** Index into snapPoints the sheet opens at. Default last. */\n initialSnapIndex?: number;\n /** Called when the resting snap point changes. */\n onSnapChange?: (index: number) => void;\n children?: ReactNode;\n}\n\nfunction resolveSnapPoints(\n snapPoints: SnapPoint[],\n windowHeight: number\n): number[] {\n return snapPoints\n .map((p) =>\n typeof p === 'string'\n ? (parseFloat(p) / 100) * windowHeight\n : p <= 1\n ? p * windowHeight\n : p\n )\n .filter((h) => h > 0 && h <= windowHeight)\n .sort((a, b) => a - b);\n}\n\nexport function Sheet({\n open,\n onOpenChange,\n showOverlay = true,\n snapPoints,\n initialSnapIndex,\n onSnapChange,\n children,\n}: SheetProps) {\n const { height: windowHeight } = useWindowDimensions();\n const heights = useMemo(\n () =>\n snapPoints?.length ? resolveSnapPoints(snapPoints, windowHeight) : null,\n [snapPoints, windowHeight]\n );\n\n const [mounted, setMounted] = useState(open);\n const [snapIndex, setSnapIndex] = useState(\n initialSnapIndex ?? (heights ? heights.length - 1 : 0)\n );\n const translateY = useRef(new Animated.Value(windowHeight)).current;\n const gestureStartY = useRef(0);\n\n const setOpenRef = useRef(onOpenChange);\n setOpenRef.current = onOpenChange;\n\n const animateTo = useCallback(\n (toValue: number, onDone?: () => void) => {\n Animated.spring(translateY, {\n toValue,\n useNativeDriver: true,\n damping: 30,\n stiffness: 300,\n mass: 0.8,\n }).start(({ finished }) => {\n if (finished) onDone?.();\n });\n },\n [translateY]\n );\n\n const restY = useCallback(\n (index: number) =>\n heights\n ? windowHeight -\n heights[Math.max(0, Math.min(index, heights.length - 1))]\n : 0,\n [heights, windowHeight]\n );\n\n // Enter / exit.\n useEffect(() => {\n if (open) {\n setMounted(true);\n animateTo(restY(snapIndex));\n } else if (mounted) {\n animateTo(windowHeight, () => setMounted(false));\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n const panHandlers = useMemo(\n () =>\n heights\n ? PanResponder.create({\n onStartShouldSetPanResponder: () => true,\n onMoveShouldSetPanResponder: (_e, g) => Math.abs(g.dy) > 4,\n onPanResponderGrant: () => {\n translateY.stopAnimation((v) => {\n gestureStartY.current = v;\n });\n },\n onPanResponderMove: (_e, g: PanResponderGestureState) => {\n const next = Math.max(0, gestureStartY.current + g.dy);\n translateY.setValue(next);\n },\n onPanResponderRelease: (_e, g) => {\n const y = Math.max(0, gestureStartY.current + g.dy);\n const sheetTop = y;\n const heightAtY = windowHeight - sheetTop;\n const lowest = heights[0];\n // Dismiss when dragged below ~2/3 of the lowest detent\n // or flicked down hard.\n if (heightAtY < lowest * 0.66 || g.vy > 1.2) {\n setOpenRef.current?.(false);\n return;\n }\n // Snap to nearest detent; upward flick prefers next higher.\n let best = 0;\n let bestDist = Infinity;\n heights.forEach((h, i) => {\n const d =\n Math.abs(heightAtY - h) -\n (g.vy < -0.5 && h > heightAtY ? h * 0.15 : 0);\n if (d < bestDist) {\n bestDist = d;\n best = i;\n }\n });\n setSnapIndex(best);\n onSnapChange?.(best);\n animateTo(windowHeight - heights[best]);\n },\n onPanResponderTerminate: () => {\n animateTo(restY(snapIndex));\n },\n }).panHandlers\n : undefined,\n [\n heights,\n windowHeight,\n translateY,\n animateTo,\n onSnapChange,\n restY,\n snapIndex,\n ]\n );\n\n const ctx = useMemo(\n () =>\n heights\n ? {\n translateY,\n panHandlers,\n sheetHeight:\n heights[Math.max(0, Math.min(snapIndex, heights.length - 1))],\n }\n : {},\n [heights, translateY, panHandlers, snapIndex]\n );\n\n // Legacy path: no snapPoints → plain slide-in sheet (unchanged behavior).\n if (!heights) {\n return (\n onOpenChange?.(false)}\n >\n \n onOpenChange?.(false)}\n >\n {children}\n \n \n \n );\n }\n\n const sheetTop = heights ? windowHeight - heights[snapIndex] : 0;\n\n return (\n onOpenChange?.(false)}\n >\n \n \n onOpenChange?.(false)}\n />\n \n \n {children}\n \n \n \n \n \n );\n}\n\nexport function SheetContent({\n className,\n children,\n ...props\n}: ViewProps & { className?: string }) {\n const { panHandlers, sheetHeight } = useContext(SheetContext);\n return (\n e.stopPropagation()}>\n \n \n \n \n {children}\n \n \n );\n}\n\nexport function SheetHeader({\n className,\n ...props\n}: ViewProps & { className?: string }) {\n return ;\n}\n\nexport function SheetTitle({ className, ...props }: TextProps) {\n return (\n \n );\n}\n\nexport function SheetDescription({ className, ...props }: TextProps) {\n return (\n \n );\n}\n\nexport function SheetFooter({\n className,\n ...props\n}: ViewProps & { className?: string }) {\n return (\n \n );\n}\n" } ] }