{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bottom-sheet",
  "title": "Bottom Sheet",
  "description": "Interactive bottom sheet built on Expo UI with native snap points, gestures, and keyboard support.",
  "dependencies": [
    "@expo/ui",
    "react-native-reanimated",
    "react-native-keyboard-controller",
    "react-native-safe-area-context"
  ],
  "registryDependencies": [
    "@tetra-ui/button",
    "@tetra-ui/icons",
    "@tetra-ui/slot"
  ],
  "files": [
    {
      "path": "ui/bottom-sheet/index.ts",
      "content": "export * from \"./bottom-sheet\";\nexport * from \"./bottom-sheet-common\";\n",
      "type": "registry:ui"
    },
    {
      "path": "ui/bottom-sheet/types.ts",
      "content": "import type { SnapPoint } from \"@expo/ui\";\nimport type { PressableProps, ViewProps } from \"react-native\";\n\nexport type BottomSheetProps = {\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  children: React.ReactNode;\n};\n\nexport type BottomSheetContentProps = ViewProps & {\n  children: React.ReactNode;\n  snapPoints?: SnapPoint[];\n  showDragIndicator?: boolean;\n  className?: string;\n};\n\nexport type BottomSheetBodyProps = {\n  children: React.ReactNode;\n  className?: string;\n};\n\nexport type BottomSheetTriggerProps = PressableProps & {\n  asChild?: boolean;\n};\n\nexport type BottomSheetCloseProps = PressableProps & {\n  asChild?: boolean;\n};\n\nexport type BottomSheetFooterProps = ViewProps;\n",
      "type": "registry:ui"
    },
    {
      "path": "ui/bottom-sheet/bottom-sheet-context.ts",
      "content": "import { createContext, useContext } from \"react\";\n\ntype BottomSheetContextValue = {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n};\n\nexport const BottomSheetContext = createContext<BottomSheetContextValue | null>(\n  null\n);\n\nexport const useBottomSheetContext = () => {\n  const context = useContext(BottomSheetContext);\n  if (!context) {\n    throw new Error(\"useBottomSheetContext must be used within an BottomSheet\");\n  }\n  return context;\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "ui/bottom-sheet/bottom-sheet-common.tsx",
      "content": "import { useCallback, useMemo, useState } from \"react\";\nimport { Pressable, ScrollView, Text, View } from \"react-native\";\nimport { cn } from \"@/lib/utils\";\nimport { Button, ButtonIcon } from \"../button\";\nimport { XIcon } from \"../icons\";\nimport { Slot } from \"../slot\";\nimport {\n  BottomSheetContext,\n  useBottomSheetContext,\n} from \"./bottom-sheet-context\";\nimport type {\n  BottomSheetCloseProps,\n  BottomSheetProps,\n  BottomSheetTriggerProps,\n} from \"./types\";\n\n// Components\nexport const BottomSheet = ({\n  open: openProp,\n  onOpenChange: onOpenChangeProp,\n  children,\n}: BottomSheetProps) => {\n  const [internalOpen, setInternalOpen] = useState(openProp ?? false);\n\n  const isControlled = openProp !== undefined;\n  const open = isControlled ? openProp : internalOpen;\n\n  const onOpenChange = useCallback(\n    (nextOpen: boolean) => {\n      if (!isControlled) {\n        setInternalOpen(nextOpen);\n      }\n      onOpenChangeProp?.(nextOpen);\n    },\n    [isControlled, onOpenChangeProp]\n  );\n\n  const ctx = useMemo(\n    () => ({\n      onOpenChange,\n      open,\n    }),\n    [open, onOpenChange]\n  );\n\n  return (\n    <BottomSheetContext.Provider value={ctx}>\n      {children}\n    </BottomSheetContext.Provider>\n  );\n};\n\nexport const BottomSheetTrigger = ({\n  asChild,\n  ...props\n}: BottomSheetTriggerProps) => {\n  const { onOpenChange } = useBottomSheetContext();\n  const Comp = asChild ? Slot.Pressable : Pressable;\n  return <Comp {...props} onPress={() => onOpenChange(true)} />;\n};\n\nexport const BottomSheetClose = ({\n  asChild,\n  ...props\n}: BottomSheetCloseProps) => {\n  const { onOpenChange } = useBottomSheetContext();\n  const Comp = asChild ? Slot.Pressable : Pressable;\n  return <Comp {...props} onPress={() => onOpenChange(false)} />;\n};\n\nexport const BottomSheetBody = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof View>) => (\n  <View className={cn(\"flex-1 px-4\", className)} {...props} />\n);\n\nexport const BottomSheetScrollView = ({\n  className,\n  contentContainerClassName,\n  ...props\n}: React.ComponentProps<typeof ScrollView>) => (\n  <ScrollView\n    contentContainerClassName={cn(\"px-4 pb-4\", contentContainerClassName)}\n    nestedScrollEnabled\n    {...props}\n  />\n);\n\nBottomSheetScrollView.displayName = \"BottomSheetScrollView\";\n\nexport const BottomSheetHeader = ({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof View>) => (\n  <View\n    className={cn(\n      \"flex flex-row items-center gap-1 px-4 ios:pt-6 pt-4.5 pb-4\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <BottomSheetClose asChild>\n      <Button className=\"ml-auto\" size=\"icon\" variant=\"link\">\n        <ButtonIcon className=\"text-muted-foreground\">\n          <XIcon />\n        </ButtonIcon>\n      </Button>\n    </BottomSheetClose>\n  </View>\n);\n\nBottomSheetHeader.displayName = \"BottomSheetHeader\";\n\nexport const BottomSheetTitle = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof Text>) => (\n  <Text\n    className={cn(\n      \"font-semibold text-foreground text-xl leading-none\",\n      className\n    )}\n    {...props}\n  />\n);\n",
      "type": "registry:ui"
    },
    {
      "path": "ui/bottom-sheet/bottom-sheet.tsx",
      "content": "import type { BottomSheetContentProps, BottomSheetFooterProps } from \"./types\";\n\nexport const BottomSheetContent = (_: BottomSheetContentProps) => {\n  return null;\n};\n\nexport const BottomSheetFooter = (_: BottomSheetFooterProps) => {\n  return null;\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "ui/bottom-sheet/bottom-sheet.ios.tsx",
      "content": "import type { SnapPoint } from \"@expo/ui\";\nimport {\n  BottomSheet as BottomSheetPrimitive,\n  Group as GroupPrimitive,\n  Host as HostPrimitive,\n  RNHostView,\n} from \"@expo/ui/swift-ui\";\nimport {\n  type ModifierConfig,\n  type PresentationDetent,\n  presentationBackground,\n  presentationDetents,\n  presentationDragIndicator,\n} from \"@expo/ui/swift-ui/modifiers\";\nimport { useWindowDimensions, View } from \"react-native\";\nimport { useReanimatedKeyboardAnimation } from \"react-native-keyboard-controller\";\nimport Animated, {\n  Extrapolation,\n  interpolate,\n  useAnimatedStyle,\n} from \"react-native-reanimated\";\nimport { useSafeAreaInsets } from \"react-native-safe-area-context\";\nimport { useCSSVariable } from \"uniwind\";\nimport { cn } from \"@/lib/utils\";\nimport { useBottomSheetContext } from \"./bottom-sheet-context\";\nimport type { BottomSheetContentProps, BottomSheetFooterProps } from \"./types\";\n\n// Constants\nconst BOTTOM_SHEET_PADDING = 16;\n\n// Components\nexport const BottomSheetContent = ({\n  showDragIndicator = true,\n  snapPoints,\n  className,\n  children,\n  style,\n  ...props\n}: BottomSheetContentProps) => {\n  const { open, onOpenChange } = useBottomSheetContext();\n  const backgroundColor = useCSSVariable(\"--color-background\") as string;\n  const hasSnapPoints = Boolean(snapPoints && snapPoints.length > 0);\n\n  const { width: windowWidth } = useWindowDimensions();\n\n  const contentModifiers: ModifierConfig[] = [\n    presentationBackground(backgroundColor),\n    presentationDragIndicator(showDragIndicator ? \"visible\" : \"hidden\"),\n  ];\n\n  if (hasSnapPoints) {\n    contentModifiers.push(\n      presentationDetents(snapPoints?.map(snapPointToDetent) || [])\n    );\n  }\n\n  return (\n    <HostPrimitive pointerEvents=\"none\" style={{ position: \"absolute\" }}>\n      <BottomSheetPrimitive\n        fitToContents={!hasSnapPoints}\n        isPresented={open}\n        onIsPresentedChange={onOpenChange}\n      >\n        <GroupPrimitive modifiers={contentModifiers}>\n          <RNHostView matchContents={!hasSnapPoints}>\n            <View\n              className={cn(\n                \"flex-1 data-[has-snap-points=true]:h-0 data-[has-snap-points=true]:grow\",\n                className\n              )}\n              data-has-snap-points={hasSnapPoints}\n              style={[{ width: windowWidth }, style]}\n              {...props}\n            >\n              {children}\n            </View>\n          </RNHostView>\n        </GroupPrimitive>\n      </BottomSheetPrimitive>\n    </HostPrimitive>\n  );\n};\n\nexport const BottomSheetFooter = ({\n  className,\n  style,\n  ...props\n}: BottomSheetFooterProps) => {\n  const { bottom } = useSafeAreaInsets();\n  const { progress: keyboardProgress } = useReanimatedKeyboardAnimation();\n\n  const animatedStyle = useAnimatedStyle(\n    () => ({\n      paddingBottom: interpolate(\n        keyboardProgress.value,\n        [0, 1],\n        [bottom ? 0 : BOTTOM_SHEET_PADDING, BOTTOM_SHEET_PADDING],\n        Extrapolation.CLAMP\n      ),\n    }),\n    [bottom]\n  );\n\n  return (\n    <Animated.View\n      className={cn(\n        \"flex flex-col gap-2 border-border border-t bg-background px-4 pt-4\",\n        className\n      )}\n      style={[animatedStyle, style]}\n      {...props}\n    />\n  );\n};\n\nBottomSheetFooter.displayName = \"BottomSheetFooter\";\n\n// Utils\nconst snapPointToDetent = (snapPoint: SnapPoint): PresentationDetent => {\n  if (snapPoint === \"half\") {\n    return \"medium\";\n  }\n  if (snapPoint === \"full\") {\n    return \"large\";\n  }\n  return snapPoint;\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "ui/bottom-sheet/bottom-sheet.android.tsx",
      "content": "import type { SnapPoint } from \"@expo/ui\";\nimport {\n  Column,\n  Host,\n  ModalBottomSheet,\n  type ModalBottomSheetRef,\n  RNHostView,\n} from \"@expo/ui/jetpack-compose\";\nimport {\n  fillMaxHeight,\n  imePadding,\n  type ModifierConfig,\n  padding,\n  weight,\n} from \"@expo/ui/jetpack-compose/modifiers\";\nimport {\n  Children,\n  Fragment,\n  isValidElement,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { Keyboard, useWindowDimensions, View } from \"react-native\";\nimport { useSafeAreaInsets } from \"react-native-safe-area-context\";\nimport { useCSSVariable } from \"uniwind\";\nimport { cn } from \"@/lib/utils\";\nimport { useBottomSheetContext } from \"./bottom-sheet-context\";\nimport type { BottomSheetContentProps, BottomSheetFooterProps } from \"./types\";\n\n// Constants\nconst BOTTOM_SHEET_PADDING = 16;\n\n// Components\nconst BottomSheetDragHandle = () => {\n  return (\n    <ModalBottomSheet.DragHandle>\n      <RNHostView matchContents>\n        <View className=\"items-center pt-2.5\">\n          <View className=\"h-1.5 w-[38px] rounded-full bg-muted-foreground\" />\n        </View>\n      </RNHostView>\n    </ModalBottomSheet.DragHandle>\n  );\n};\n\nexport const BottomSheetContent = ({\n  showDragIndicator = true,\n  snapPoints,\n  className,\n  children,\n  style,\n  ...props\n}: BottomSheetContentProps) => {\n  const { open, onOpenChange } = useBottomSheetContext();\n\n  const sheetRef = useRef<ModalBottomSheetRef>(null);\n  const [visible, setVisible] = useState(open);\n\n  const backgroundColor = useCSSVariable(\"--color-background\") as string;\n\n  const { width: windowWidth } = useWindowDimensions();\n\n  const { body, footer, header } = useMemo(\n    () => splitBottomSheetChildren(children),\n    [children]\n  );\n\n  const hasSnapPoints = Boolean(snapPoints && snapPoints.length > 0);\n  const hasFooter = Boolean(footer);\n\n  useEffect(() => {\n    if (open) {\n      setVisible(true);\n      return;\n    }\n\n    let cancelled = false;\n    sheetRef.current?.hide().then(() => {\n      if (!cancelled) {\n        setVisible(false);\n      }\n    });\n\n    return () => {\n      cancelled = true;\n    };\n  }, [open]);\n\n  const handleDismiss = useCallback(() => {\n    onOpenChange(false);\n  }, [onOpenChange]);\n\n  const contentModifiers = useMemo(() => {\n    const modifiers: ModifierConfig[] = [];\n\n    if (shouldFillMaxHeight(snapPoints)) {\n      modifiers.push(fillMaxHeight(0.95));\n    }\n\n    if (hasFooter) {\n      modifiers.push(imePadding());\n    }\n\n    return modifiers;\n  }, [hasFooter, snapPoints]);\n\n  useEffect(() => {\n    if (!(open && hasFooter)) {\n      return;\n    }\n\n    const showSub = Keyboard.addListener(\"keyboardDidShow\", () => {\n      sheetRef.current?.expand();\n    });\n\n    return () => {\n      showSub.remove();\n    };\n  }, [hasFooter, open]);\n\n  if (!visible) {\n    return null;\n  }\n\n  return (\n    <Host\n      pointerEvents=\"none\"\n      style={{ position: \"absolute\", width: windowWidth }}\n    >\n      <ModalBottomSheet\n        containerColor={backgroundColor}\n        onDismissRequest={handleDismiss}\n        ref={sheetRef}\n        showDragHandle={false}\n        skipPartiallyExpanded={shouldSkipPartiallyExpanded(snapPoints)}\n      >\n        {showDragIndicator ? <BottomSheetDragHandle /> : null}\n\n        <Column modifiers={contentModifiers}>\n          <Column modifiers={hasSnapPoints ? [weight(1)] : undefined}>\n            <RNHostView matchContents={!hasSnapPoints}>\n              <View {...props}>\n                {header}\n                {body}\n              </View>\n            </RNHostView>\n          </Column>\n          {footer}\n        </Column>\n      </ModalBottomSheet>\n    </Host>\n  );\n};\n\nexport const BottomSheetFooter = ({\n  className,\n  style,\n  children,\n  ...props\n}: BottomSheetFooterProps) => {\n  const { bottom: safeAreaBottom } = useSafeAreaInsets();\n\n  return (\n    <RNHostView\n      matchContents\n      modifiers={[\n        padding(0, 0, 0, Math.max(safeAreaBottom + BOTTOM_SHEET_PADDING)),\n      ]}\n    >\n      <View\n        className={cn(\n          \"flex flex-col gap-2 border-border border-t bg-background px-4 pt-4\",\n          className\n        )}\n        style={style}\n        {...props}\n      >\n        {children}\n      </View>\n    </RNHostView>\n  );\n};\n\nBottomSheetFooter.displayName = \"BottomSheetFooter\";\n\n// Utils\nconst shouldSkipPartiallyExpanded = (\n  snapPoints: SnapPoint[] | undefined\n): boolean => {\n  if (!snapPoints || snapPoints.length === 0) {\n    return false;\n  }\n\n  return !snapPoints.some(\n    (snapPoint) =>\n      snapPoint === \"half\" ||\n      (typeof snapPoint === \"object\" &&\n        \"fraction\" in snapPoint &&\n        snapPoint.fraction < 1) ||\n      (typeof snapPoint === \"object\" && \"height\" in snapPoint)\n  );\n};\n\nconst shouldFillMaxHeight = (snapPoints: SnapPoint[] | undefined): boolean => {\n  if (!snapPoints || snapPoints.length === 0) {\n    return false;\n  }\n\n  return snapPoints.some(\n    (snapPoint) =>\n      snapPoint === \"full\" ||\n      (typeof snapPoint === \"object\" &&\n        \"fraction\" in snapPoint &&\n        snapPoint.fraction >= 1)\n  );\n};\n\nconst getChildDisplayName = (child: React.ReactNode) => {\n  if (!isValidElement(child)) {\n    return;\n  }\n  return (child.type as { displayName?: string }).displayName;\n};\n\nconst flattenChildren = (children: React.ReactNode): React.ReactNode[] => {\n  const flattened: React.ReactNode[] = [];\n\n  Children.forEach(children, (child) => {\n    if (child === null || child === undefined || typeof child === \"boolean\") {\n      return;\n    }\n\n    if (isValidElement(child) && child.type === Fragment) {\n      flattened.push(\n        ...flattenChildren(\n          (child.props as { children?: React.ReactNode }).children\n        )\n      );\n      return;\n    }\n\n    flattened.push(child);\n  });\n\n  return flattened;\n};\n\nconst splitBottomSheetChildren = (children: React.ReactNode) => {\n  const body: React.ReactNode[] = [];\n  let footer: React.ReactNode = null;\n  let header: React.ReactNode = null;\n\n  for (const child of flattenChildren(children)) {\n    const displayName = getChildDisplayName(child);\n\n    if (displayName === \"BottomSheetFooter\") {\n      footer = child;\n      continue;\n    }\n\n    if (displayName === \"BottomSheetHeader\") {\n      header = child;\n      continue;\n    }\n\n    body.push(child);\n  }\n\n  return { body, footer, header };\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
