main 65f1c49bcc59 cached
50 files
88.9 KB
22.3k tokens
40 symbols
1 requests
Download .txt
Repository: stanleyugwu/react-native-bottom-sheet
Branch: main
Commit: 65f1c49bcc59
Files: 50
Total size: 88.9 KB

Directory structure:
gitextract_pza16rl0/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── actions/
│   │   └── setup/
│   │       └── action.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .nvmrc
├── .watchmanconfig
├── .yarnrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── babel.config.js
├── commitlint.config.ts
├── example/
│   ├── App.js
│   ├── app.json
│   ├── babel.config.js
│   ├── metro.config.js
│   ├── package.json
│   ├── src/
│   │   └── App.tsx
│   ├── tsconfig.json
│   └── webpack.config.js
├── lefthook.yml
├── package.json
├── scripts/
│   ├── bootstrap.js
│   ├── copy-dts.js
│   └── delete-lib-dir.js
├── src/
│   ├── __tests__/
│   │   └── index.test.tsx
│   ├── components/
│   │   ├── animatedTouchableBackdropMask/
│   │   │   ├── index.tsx
│   │   │   └── types.d.ts
│   │   ├── backdrop/
│   │   │   ├── index.tsx
│   │   │   └── types.d.ts
│   │   ├── bottomSheet/
│   │   │   ├── index.tsx
│   │   │   └── types.d.ts
│   │   ├── container/
│   │   │   └── index.tsx
│   │   └── defaultHandleBar/
│   │       ├── index.tsx
│   │       └── types.d.ts
│   ├── constant/
│   │   └── index.ts
│   ├── hooks/
│   │   ├── useAnimatedValue.ts
│   │   ├── useHandleAndroidBackButtonClose/
│   │   │   ├── index.ts
│   │   │   └── types.d.ts
│   │   └── useHandleKeyboardEvents/
│   │       ├── index.ts
│   │       └── types.d.ts
│   ├── index.ts
│   ├── types.d.ts
│   └── utils/
│       ├── convertHeight.ts
│       ├── normalizeHeight.ts
│       └── separatePaddingStyles.ts
├── tsconfig.build.json
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org

root = true

[*]

indent_style = space
indent_size = 2

end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: .gitattributes
================================================
*.pbxproj -text
# specific for windows script files
*.bat text eol=crlf

================================================
FILE: .github/actions/setup/action.yml
================================================
name: Setup
description: Setup Node.js and install dependencies

runs:
  using: composite
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version-file: .nvmrc

    - name: Cache dependencies
      id: yarn-cache
      uses: actions/cache@v3
      with:
        path: |
          **/node_modules
        key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }}
        restore-keys: |
          ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          ${{ runner.os }}-yarn-

    - name: Install dependencies
      if: steps.yarn-cache.outputs.cache-hit != 'true'
      run: |
        yarn install --cwd example --frozen-lockfile
        yarn install --frozen-lockfile
      shell: bash


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup
        uses: ./.github/actions/setup

      - name: Lint files
        run: yarn lint

      - name: Typecheck files
        run: yarn typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup
        uses: ./.github/actions/setup

      - name: Run unit tests
        run: yarn test --maxWorkers=2 --coverage

  build-library:
    runs-on: macos-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup
        uses: ./.github/actions/setup

      - name: Build package
        run: yarn prepack

  build-web:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup
        uses: ./.github/actions/setup

      - name: Build example for Web
        run: |
          yarn example expo export:web


================================================
FILE: .gitignore
================================================
# OSX
#
.DS_Store

# XDE
.expo/

# VSCode
.vscode/
jsconfig.json

# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace

# Android/IJ
#
.classpath
.cxx
.gradle
.idea
.project
.settings
local.properties
android.iml

# Cocoapods
#
example/ios/Pods

# Ruby
example/vendor/

# node.js
#
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
.env

# BUCK
buck-out/
\.buckd/
android/app/libs
android/keystores/debug.keystore

# Expo
.expo/

# Turborepo
.turbo/

# generated by bob
lib/

# user files
backlog.md


================================================
FILE: .nvmrc
================================================
v18


================================================
FILE: .watchmanconfig
================================================
{}

================================================
FILE: .yarnrc
================================================
# Override Yarn command so we can automatically setup the repo on running `yarn`

yarn-path "scripts/bootstrap.js"


================================================
FILE: CODE_OF_CONDUCT.md
================================================

# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
  community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or advances of
  any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
  without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series of
actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within the
community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].

Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].

For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].

[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

Contributions are always welcome, no matter how large or small!

We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md).

## Development workflow

To get started with the project, run `yarn` in the root directory to install the required dependencies for each package:

```sh
yarn
```

> While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development.

While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app.

To start the packager:

```sh
yarn example start
```

To run the example app on Android:

```sh
yarn example android
```

To run the example app on iOS:

```sh
yarn example ios
```

To run the example app on Web:

```sh
yarn example web
```

Make sure your code passes TypeScript and ESLint. Run the following to verify:

```sh
yarn typecheck
yarn lint
```

To fix formatting errors, run the following:

```sh
yarn lint --fix
```

Remember to add tests for your change if possible. Run the unit tests by:

```sh
yarn test
```


### Commit message convention

We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages:

- `fix`: bug fixes, e.g. fix crash due to deprecated method.
- `feat`: new features, e.g. add new method to the module.
- `refactor`: code refactor, e.g. migrate from class components to hooks.
- `docs`: changes into documentation, e.g. add usage example for the module..
- `test`: adding or updating tests, e.g. add integration tests using detox.
- `chore`: tooling changes, e.g. change CI config.
- `perf`: changes that improve performance
- `improvement`: changes that improve a current implementation
- `style`: syntax formatting


Our pre-commit hooks verify that your commit message matches this format when committing.

### Linting and tests

[ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/)

We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing.

Our pre-commit hooks verify that the linter and tests pass when committing.

### Publishing to npm

We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc.

To publish new versions, run the following:

```sh
yarn release
```

### Scripts

The `package.json` file contains various scripts for common tasks:

- `yarn bootstrap`: setup project by installing all dependencies and pods.
- `yarn typecheck`: type-check files with TypeScript.
- `yarn lint`: lint files with ESLint.
- `yarn test`: run unit tests with Jest.
- `yarn example start`: start the Metro server for the example app.
- `yarn example android`: run the example app on Android.
- `yarn example ios`: run the example app on iOS.

### Sending a pull request

> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github).

When you're sending a pull request:

- Prefer small pull requests focused on one change.
- Verify that linters and tests are passing.
- Review the documentation to make sure it looks good.
- Follow the pull request template when opening a pull request.
- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue.

Thanks for your contributions 💖


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 Devvie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# React Native Bottom Sheet 💖

![GitHub](https://img.shields.io/github/license/stanleyugwu/react-native-bottom-sheet?style=plastic&label=License&color=%23fea9f8)
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/stanleyugwu/react-native-bottom-sheet/ci.yml?color=%23fea9f8&label=Build)
[![runs with expo](https://img.shields.io/badge/Expo-Support-fea9f8.svg?style=platic&logo=EXPO&logoColor=fff)](https://expo.io/)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/stanleyugwu/react-native-bottom-sheet?color=%23fea9f8&label=Code%20Size)
![npm bundle size (scoped)](https://img.shields.io/bundlephobia/minzip/@devvie/bottom-sheet?style=plastic&logo=npm&color=%23fea9f8&label=Bundle%20Size)
![npm downloads](https://img.shields.io/npm/dm/@devvie/bottom-sheet?style=plastic&logo=npm&color=%23fea9f8&label=Downloads)

The smart 😎, tiny 📦, and flexible 🎗 bottom sheet your app craves 🚀

---
👉🏾
<a href="https://www.buymeacoffee.com/devvie" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
👈🏾
---

![Preview for Android & iOS](https://i.ibb.co/Y38XsMr/Combined.gif)

#### Web Preview

<p float="left">
  <img src="https://i.ibb.co/sJpsFKD/Web.png" width="400" />
</p>

## ✨Features

- 📦 Very tiny and lightweight
- 0️⃣ No dependency (yeah!, just plug and play 😎)
- ✨ Modal and standard (non-modal) bottom sheet support
- ⌨ Smart & automatic keyboard and orientation handling for iOS & Android
- 💪 Imperative calls
- 📜 Supports FlatList, SectionList, ScrollView & View scrolling interactions
- 📟 Handles layout & orientation changes smartly
- 💯 Compatible with Expo
- 🔧 Flexible config
- 🚀 Supports props live update
- 🎞 Configurable animation
- 🎨 Follows Material Design principles
- 🌐 Runs on the web
- ✅ Written in TypeScript

## 💻 Installation

```sh
npm install @devvie/bottom-sheet
```

or

```sh
yarn add @devvie/bottom-sheet
```

## 📱 Minimal Usage

Opening and closing the bottom sheet is done imperatively, so just pass a `ref` to the bottom sheet and call the `open` or `close` methods via the `ref` instance to open and close the bottom sheet respectively.

##### Examples

#### Typescript

```tsx
import React, { useRef } from 'react';
import BottomSheet, { BottomSheetMethods } from '@devvie/bottom-sheet';
import { Button, View } from 'react-native';

const App = () => {
  const sheetRef = useRef<BottomSheetMethods>(null);
  return (
    <View>
      <Button title="Open" onPress={() => sheetRef.current?.open()} />
      <BottomSheet ref={sheetRef}>
        <Text>
          The smart 😎, tiny 📦, and flexible 🎗 bottom sheet your app craves 🚀
        </Text>
      </BottomSheet>
    </View>
  );
};

export default App;
```

#### Javascript

```tsx
import React, { useRef } from 'react';
import BottomSheet, { BottomSheetMethods } from '@devvie/bottom-sheet';
import { Button, View } from 'react-native';

const App = () => {
  const sheetRef = useRef(null);
  return (
    <View>
      <Button title="Open" onPress={() => sheetRef.current?.open()} />
      <BottomSheet ref={sheetRef}>
        <Text>
          The smart 😎, tiny 📦, and flexible 🎗 bottom sheet your app craves 🚀
        </Text>
      </BottomSheet>
    </View>
  );
};
```

### ⚠ Warning

The bottom sheet component uses and handles pan gestures internally, so to avoid scroll/pan misbehavior with its container, **DO NOT** put it inside a container that supports panning e.g `ScrollView`. You can always put it just next to the `ScrollView` and use `React Fragment` or a `View` to wrap them and everything should be okay.

#### ❌ Don't do this

```jsx
<ScrollView>
  <BottomSheet>...</BottomSheet>
</ScrollView>
```

#### ✅ Do this

```jsx
<>
  <ScrollView>...</ScrollView>

  <BottomSheet>...</BottomSheet>
</>
```

## 🛠 Props

The bottom sheet is highly configurable via props. All props works for both `Android` and `iOS` except those prefixed with `android_` and `ios_`, which works for only `Android` and `iOS` respectively.

| Property                          | Type                                                                                    | Default                | Description                                                                                                                                       | Required |
| --------------------------------- | --------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `android_backdropMaskRippleColor` | `string \| OpaqueColorValue`                                                            |                        | Color of the ripple effect when backdrop mask is pressed (**Android Only**).                                                                      | No       |
| `android_closeOnBackPress`        | `boolean`                                                                               | `true`                 | Determines whether the sheet will close when the device back button is pressed (**Android Only**).                                                | No       |
| `animationType`                   | `'slide' \| 'spring' \| 'fade' \| ANIMATIONS`                                           | `'slide'`              | Animation to use when opening and closing the bottom sheet.                                                                                       | No       |
| `backdropMaskColor`               | `string \| OpaqueColorValue`                                                            | `'#00000052'`          | Color of the scrim or backdrop mask.                                                                                                              | No       |
| `children`                        | `ViewProps['children'] \| React.FunctionComponent<{_animatedHeight: Animated.Value}>`   | `null`                 | Contents of the bottom sheet.                                                                                                                     | Yes      |
| `closeDuration`                   | `number`                                                                                | `500`                  | Duration for sheet closing animation.                                                                                                             | No       |
| `closeOnBackdropPress`            | `boolean`                                                                               | `true`                 | Determines whether the bottom sheet will close when the scrim or backdrop mask is pressed.                                                        | No       |
| `closeOnDragDown`                 | `boolean`                                                                               | `true`                 | Determines whether the bottom sheet will close when dragged down.                                                                                 | No       |
| `containerHeight`                 | `ViewStyle['height']`                                                                   | `DEVICE SCREEN HEIGHT` | Height of the bottom sheet's overall container.                                                                                                   | No       |
| `customBackdropComponent`         | `React.FunctionComponent<{_animatedHeight: Animated.Value}>`                            | `null`                 | Custom component for sheet's scrim or backdrop mask.                                                                                              | No       |
| `customBackdropPosition`          | `"top" \| "behind"`                                                                     | `'behind'`             | Determines the position of the custom scrim or backdrop component. `'behind'` puts it behind the keyboard and `'top'`` puts it atop the keyboard. | No       |
| `customDragHandleComponent`       | `React.FC<{_animatedHeight: Animated.Value}>`                                           |                        | Custom drag handle component to replace the default bottom sheet's drag handle.                                                                   | No       |
| `customEasingFunction`            | `AnimationEasingFunction`                                                               | `ANIMATIONS.SLIDE`     | Custom easing function for driving sheet's animation.                                                                                             | No       |
| `disableBodyPanning`              | `boolean`                                                                               | `false`                | Prevents the bottom sheet from being dragged/panned down on its body.                                                                             | No       |
| `disableDragHandlePanning`        | `boolean`                                                                               | `false`                | Prevents the bottom sheet from being panned down by dragging its drag handle.                                                                     | No       |
| `dragHandleStyle`                 | `ViewStyle`                                                                             |                        | Extra styles to apply to the drag handle.                                                                                                         | No       |
| `height`                          | `number \| string`                                                                      | `'50%'`                | Height of the bottom sheet when opened. Relative to `containerHeight` prop                                                                        | No       |
| `hideDragHandle`                  | `boolean`                                                                               | `false`                | When true, hides the sheet's drag handle.                                                                                                         | No       |
| `modal`                           | `boolean`                                                                               | `true`                 | Determines whether the sheet is a modal. A modal sheet has a scrim or backdrop mask, while a standard (non-modal) sheet doesn't.                  | No       |
| `openDuration`                    | `number`                                                                                | `500`                  | Duration for sheet opening animation.                                                                                                             | No       |
| `style`                           | `Omit<ViewStyle, 'height' \| 'minHeight' \| 'maxHeight' \| 'transform:[{translateY}]'>` |                        | Extra styles to apply to the bottom sheet.                                                                                                        | No       |

## Examples

Flexibility is a focus for this bottom sheet, these few examples shows certain behaviors of the bottom sheet and what can be achieved by tweaking its props.

### 1️⃣ Smart response to keyboard pop ups and orientation changes (_automatic behavior_)

|                                          Android                                           |                                          iOS                                           |
| :----------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------: |
| ![Preview for keyboard handling (Android)](https://i.ibb.co/0BfLWYK/Keyboard-Response.gif) | ![Preview for keyboard handling (iOS)](https://i.ibb.co/302ZYBL/Keyboard-Response.gif) |

### 2️⃣ Handles deeply nested list and scroll views interactions (_automatic beavior_)

|                                       Android                                        |                                      iOS                                       |
| :----------------------------------------------------------------------------------: | :----------------------------------------------------------------------------: |
| ![Preview for scroll handling (Android)](https://i.ibb.co/kgfPM3w/Nested-Scroll.gif) | ![Preview for scroll handling (iOS)](https://i.ibb.co/rcrJVLc/Nested-List.gif) |

### 3️⃣ Auto adjusts layout when `height` and `containerHeight` props change (_automatic behavior_)

<p float="left">
  <img src="https://i.ibb.co/3YGXHht/Detect-Height.gif" width="300" />
</p>

### 4️⃣ Extend sheet height when its content is scrolled

<p float="left">
  <img src="https://i.ibb.co/9W5J2t5/Extend-on-scroll.gif" width="300" />
</p>

### 5️⃣ Use as `SnackBar`

<p float="left">
  <img src="https://i.ibb.co/LkMJ255/Snack-Bar.gif" width="300" />
</p>

### 6️⃣ Custom Drag Handle Animation Interpolation

<p float="left">
  <img src="https://i.ibb.co/0yDPQ0W/Drag-Handle-Animation.gif" width="300" />
</p>

### 7️⃣ Custom Scrim/Backdrop Mask

<p float="left">
  <img src="https://i.ibb.co/h9XqBJC/Custom-Scrim.gif" width="300" />
</p>

#### _More Examples and code samples coming soon..._

## Contributing

See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.

## License

MIT

see [LICENSE](LICENSE.md)

---

## Support

<a href="https://www.buymeacoffee.com/devvie"><img style="height: 50px !important;align:center;width: 217px !important" src="https://img.buymeacoffee.com/button-api/?text=Buy me Okpa&emoji=🍘&slug=devvie&button_colour=Fea9f8&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff" /></a>

</> with 💖 by [Devvie](https://twitter.com/stanleyugwu_) ✌


================================================
FILE: babel.config.js
================================================
module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
};


================================================
FILE: commitlint.config.ts
================================================
import type { UserConfig } from '@commitlint/types';

const Configuration: UserConfig = {
  extends: ['@commitlint/config-conventional'],
  parserPreset: {
    parserOpts: {
      headerPattern:
        /^(?<type>.*\s\w*)(?:\((?<scope>.*)\))?!?:\s(?<subject>(?:(?!#).)*(?:(?!\s).))$/,
      headerCorrespondence: ['type', 'scope', 'subject'],
    },
  },
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'build',
        'chore',
        'ci',
        'docs',
        'feat',
        'fix',
        'perf',
        'refactor',
        'revert',
        'style',
        'test',
        '🛠️ build',
        '🛠️🚀 build', // new version release
        '♻️ chore',
        '⚙️ ci',
        '📃 docs',
        '✨ feat',
        '🐞 fix',
        '🚀 perf',
        '🦄 refactor',
        '🗑️ revert',
        '🌈 style',
        '🧪 test',
      ],
    ],
  },
};

module.exports = Configuration;


================================================
FILE: example/App.js
================================================
export { default } from './src/App';


================================================
FILE: example/app.json
================================================
{
  "expo": {
    "name": "example",
    "slug": "example",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "light",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      }
    },
    "web": {
      "favicon": "./assets/favicon.png"
    }
  }
}


================================================
FILE: example/babel.config.js
================================================
const path = require('path');
const pak = require('../package.json');

module.exports = function (api) {
  api.cache(true);

  return {
    presets: ['babel-preset-expo'],
    plugins: [
      [
        'module-resolver',
        {
          extensions: ['.tsx', '.ts', '.js', '.json'],
          alias: {
            // For development, we want to alias the library to the source
            [pak.name]: path.join(__dirname, '..', pak.source),
          },
        },
      ],
    ],
  };
};


================================================
FILE: example/metro.config.js
================================================
const path = require('path');
const escape = require('escape-string-regexp');
const { getDefaultConfig } = require('@expo/metro-config');
const exclusionList = require('metro-config/src/defaults/exclusionList');
const pak = require('../package.json');

const root = path.resolve(__dirname, '..');
const modules = Object.keys({ ...pak.peerDependencies });

const defaultConfig = getDefaultConfig(__dirname);

/**
 * Metro configuration
 * https://facebook.github.io/metro/docs/configuration
 *
 * @type {import('metro-config').MetroConfig}
 */
const config = {
  ...defaultConfig,

  projectRoot: __dirname,
  watchFolders: [root],

  // We need to make sure that only one version is loaded for peerDependencies
  // So we block them at the root, and alias them to the versions in example's node_modules
  resolver: {
    ...defaultConfig.resolver,

    blacklistRE: exclusionList(
      modules.map(
        (m) =>
          new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`)
      )
    ),

    extraNodeModules: modules.reduce((acc, name) => {
      acc[name] = path.join(__dirname, 'node_modules', name);
      return acc;
    }, {}),
  },
};

module.exports = config;


================================================
FILE: example/package.json
================================================
{
  "name": "example",
  "version": "1.0.0",
  "main": "node_modules/expo/AppEntry.js",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "@devvie/bottom-sheet": "^0.1.2",
    "expo": "~49.0.11",
    "expo-status-bar": "~1.6.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "0.72.4",
    "react-native-web": "~0.19.6"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@expo/webpack-config": "^18.0.1",
    "babel-loader": "^8.1.0",
    "babel-plugin-module-resolver": "^5.0.0"
  },
  "private": true
}


================================================
FILE: example/src/App.tsx
================================================
import * as React from 'react';
import { StyleSheet, View, Text, Button } from 'react-native';
// @ts-ignore ts can't find module '@devvie/bottom-sheet' on github runner because it's aliased
import BottomSheet, { type BottomSheetMethods } from '@devvie/bottom-sheet';

export default function App() {
  const sheetRef = React.useRef<BottomSheetMethods>(null);

  return (
    <View style={styles.container}>
      <Button title="Open Sheet" onPress={() => sheetRef.current?.open()} />
      <BottomSheet ref={sheetRef}>
        <Text style={styles.text}>
          The 😎smart, 📦tiny, and 🎗flexible bottom sheet your app craves
        </Text>
      </BottomSheet>
    </View>
  );
}

const styles = StyleSheet.create({
  text: {
    textAlign: 'center',
    fontSize: 20,
    fontWeight: '700',
    padding: 20,
  },
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});


================================================
FILE: example/tsconfig.json
================================================
{
  "extends": "../tsconfig",
  "compilerOptions": {
    // Avoid expo-cli auto-generating a tsconfig
  }
}


================================================
FILE: example/webpack.config.js
================================================
const path = require('path');
const createExpoWebpackConfigAsync = require('@expo/webpack-config');
const { resolver } = require('./metro.config');

const root = path.resolve(__dirname, '..');
const node_modules = path.join(__dirname, 'node_modules');

module.exports = async function (env, argv) {
  const config = await createExpoWebpackConfigAsync(env, argv);

  config.module.rules.push({
    test: /\.(js|jsx|ts|tsx)$/,
    include: path.resolve(root, 'src'),
    use: 'babel-loader',
  });

  // We need to make sure that only one version is loaded for peerDependencies
  // So we alias them to the versions in example's node_modules
  Object.assign(config.resolve.alias, {
    ...resolver.extraNodeModules,
    'react-native-web': path.join(node_modules, 'react-native-web'),
  });

  return config;
};


================================================
FILE: lefthook.yml
================================================
pre-commit:
  parallel: true
  commands:
    lint:
      files: git diff --name-only @{push}
      glob: "*.{js,ts,jsx,tsx}"
      run: npx eslint {files}
    types:
      files: git diff --name-only @{push}
      glob: "*.{js,ts, jsx, tsx}"
      run: npx tsc --noEmit
commit-msg:
  parallel: true
  commands:
    commitlint:
      run: npx commitlint --edit


================================================
FILE: package.json
================================================
{
  "name": "@devvie/bottom-sheet",
  "version": "0.4.3",
  "description": "The 😎smart , 📦tiny , and 🎗flexible bottom sheet your app craves 🚀",
  "main": "lib/commonjs/index",
  "module": "lib/module/index",
  "types": "lib/typescript/src/index.d.ts",
  "react-native": "src/index",
  "source": "src/index",
  "files": [
    "src",
    "lib",
    "scripts"
  ],
  "scripts": {
    "test": "jest",
    "typecheck": "tsc --noEmit",
    "lint": "eslint \"**/*.{js,ts,tsx}\"",
    "del-build-dir": "node scripts/delete-lib-dir.js",
    "build-dts": "tsc --project tsconfig.build.json",
    "copy-dts:unix": "mkdir -p lib/typescript && rsync --prune-empty-dirs -av --include '*/' --include '*.d.ts' --exclude '*' src/ lib/typescript/",
    "copy-dts:windows": "xcopy /S /Y \"src\\*.d.ts\" \"lib\\typescript\"",
    "copy-dts": "if [ \"$OS\" = \"Windows_NT\" ]; then yarn copy-dts:windows; else yarn copy-dts:unix; fi",
    "prepack": "yarn del-build-dir && yarn build-dts && yarn copy-dts && bob build",
    "release": "yarn prepack && dotenv release-it --",
    "example": "yarn --cwd example",
    "bootstrap": "yarn example && yarn install"
  },
  "keywords": [
    "react-native",
    "ios",
    "android",
    "bottom-sheet",
    "react-native-bottom-sheet",
    "devvie-bottom-sheet",
    "@devvie/bottom-sheet",
    "tiny-bottom-sheet",
    "flexible-bottom-sheet",
    "modal-bottom-sheet",
    "sheet",
    "ui-sheet"
  ],
  "repository": "https://github.com/stanleyugwu/react-native-bottom-sheet",
  "author": "Devvie <stanleyugwu2018@gmail.com> (https://github.com/stanleyugwu)",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/stanleyugwu/react-native-bottom-sheet/issues"
  },
  "homepage": "https://github.com/stanleyugwu/react-native-bottom-sheet#readme",
  "publishConfig": {
    "registry": "https://registry.npmjs.org/",
    "access": "public"
  },
  "devDependencies": {
    "@commitlint/config-conventional": "^17.0.2",
    "@evilmartians/lefthook": "^1.2.2",
    "@react-native/eslint-config": "^0.72.2",
    "@release-it/conventional-changelog": "^5.0.0",
    "@types/jest": "^28.1.2",
    "@types/react": "~18.2.0",
    "@types/react-native": "0.70.0",
    "commitlint": "^17.0.2",
    "del-cli": "^5.0.0",
    "dotenv-cli": "^7.3.0",
    "eslint": "^8.4.1",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-prettier": "^4.0.0",
    "jest": "^28.1.1",
    "pod-install": "^0.1.0",
    "prettier": "^2.0.5",
    "react": "18.2.0",
    "react-native": "0.72.4",
    "react-native-builder-bob": "^0.23.1",
    "release-it": "^15.0.0",
    "typescript": "^5.0.2"
  },
  "resolutions": {
    "@types/react": "18.2.0"
  },
  "peerDependencies": {
    "react": "*",
    "react-native": "*"
  },
  "packageManager": "yarn@1.22.19",
  "jest": {
    "preset": "react-native",
    "modulePathIgnorePatterns": [
      "<rootDir>/example/node_modules",
      "<rootDir>/lib/"
    ]
  },
  "release-it": {
    "git": {
      "commitMessage": "🛠️🚀 build: release ${version}",
      "tagName": "v${version}",
      "requireBranch": "main"
    },
    "npm": {
      "publish": true
    },
    "github": {
      "release": true,
      "releaseNotes": "git log --no-merges --pretty=format:\"* %s %h\" ${latestTag}...main",
      "comments": {
        "submit": true,
        "issue": ":rocket: _This issue has been resolved in v${version}. See [${releaseName}](${releaseUrl}) for release notes._",
        "pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
      }
    },
    "plugins": {
      "@release-it/conventional-changelog": {
        "preset": "angular"
      }
    }
  },
  "eslintConfig": {
    "root": true,
    "extends": [
      "@react-native",
      "prettier"
    ],
    "rules": {
      "prettier/prettier": [
        "error",
        {
          "quoteProps": "consistent",
          "singleQuote": true,
          "tabWidth": 2,
          "trailingComma": "es5",
          "useTabs": false,
          "endOfLine": "auto"
        }
      ]
    }
  },
  "eslintIgnore": [
    "node_modules/",
    "lib/"
  ],
  "prettier": {
    "quoteProps": "consistent",
    "singleQuote": true,
    "tabWidth": 2,
    "trailingComma": "es5",
    "useTabs": false
  },
  "react-native-builder-bob": {
    "source": "src",
    "output": "lib",
    "targets": [
      "commonjs",
      "module"
    ]
  }
}


================================================
FILE: scripts/bootstrap.js
================================================
const os = require('os');
const path = require('path');
const child_process = require('child_process');

const root = path.resolve(__dirname, '..');
const args = process.argv.slice(2);
const options = {
  cwd: process.cwd(),
  env: process.env,
  stdio: 'inherit',
  encoding: 'utf-8',
};

if (os.type() === 'Windows_NT') {
  options.shell = true;
}

let result;

if (process.cwd() !== root || args.length) {
  // We're not in the root of the project, or additional arguments were passed
  // In this case, forward the command to `yarn`
  result = child_process.spawnSync('yarn', args, options);
} else {
  // If `yarn` is run without arguments, perform bootstrap
  result = child_process.spawnSync('yarn', ['bootstrap'], options);
}

process.exitCode = result.status;


================================================
FILE: scripts/copy-dts.js
================================================
const fs = require('fs-extra');
const path = require('path');

const sourceDirectory = './src'; // Change this to your source directory
const destinationDirectory = './lib/typescript'; // Change this to your destination directory

// Ensure the destination directory exists, create it if it doesn't
fs.ensureDirSync(destinationDirectory);

// Function to copy .d.ts files recursively
function copyDeclarationFiles(src, dest) {
  const files = fs.readdirSync(src);

  files.forEach((file) => {
    const sourceFilePath = path.join(src, file);
    const destinationFilePath = path.join(dest, file);

    if (fs.statSync(sourceFilePath).isDirectory()) {
      // If it's a directory, copy its contents recursively
      fs.ensureDirSync(destinationFilePath);
      copyDeclarationFiles(sourceFilePath, destinationFilePath);
    } else if (path.extname(file) === '.d.ts') {
      // If it's a .d.ts file, copy it to the destination directory
      fs.copyFileSync(sourceFilePath, destinationFilePath);
    }
  });
}

// Start copying recursively
copyDeclarationFiles(sourceDirectory, destinationDirectory);

console.log('Declaration files copied recursively.');


================================================
FILE: scripts/delete-lib-dir.js
================================================
const fs = require('fs');
const os = require('os');
const { exec } = require('child_process');

// Define the relative directory path
const directoryName = 'lib';

// Function to check if a directory exists
const directoryExists = (directory) => {
  try {
    fs.accessSync(directory, fs.constants.F_OK);
    return true;
  } catch (err) {
    return false;
  }
};

// Detect the operating system
const platform = os.platform();

// Conditional execution based on the detected OS
if (directoryExists(directoryName)) {
  if (platform === 'win32') {
    // Windows
    exec(`rmdir /s /q "${directoryName}"`, (error, stdout, stderr) => {
      if (error) {
        console.error(`Error: ${error.message}`);
        return;
      }
    });
  } else if (platform === 'linux' || platform === 'darwin') {
    // Linux or macOS
    exec(`rm -rf "${directoryName}"`, (error, stdout, stderr) => {
      if (error) {
        console.error(`Error: ${error.message}`);
        return;
      }
    });
  } else {
    console.error('Unsupported operating system');
  }
}


================================================
FILE: src/__tests__/index.test.tsx
================================================
it.todo('write a test');


================================================
FILE: src/components/animatedTouchableBackdropMask/index.tsx
================================================
import React from 'react';
import { Animated, Pressable, StyleSheet } from 'react-native';
import { type AnimatedTouchableBackdropMaskProps } from './types.d';

/**
 * Polymorphic and re-usable animated backdrop mask component
 */
const _AnimatedTouchableBackdropMask =
  Animated.createAnimatedComponent(Pressable);

const AnimatedTouchableBackdropMask = ({
  style,
  isPressable,
  pressHandler,
  android_touchRippleColor,
  ...otherProps
}: AnimatedTouchableBackdropMaskProps) => {
  return isPressable ? (
    <_AnimatedTouchableBackdropMask
      style={[style, styles.sharedBackdropStyle]}
      android_ripple={
        android_touchRippleColor
          ? {
              borderless: true,
              color: android_touchRippleColor,
              foreground: true,
            }
          : undefined
      }
      onPress={pressHandler}
      {...otherProps}
    />
  ) : (
    // @ts-expect-error
    <Animated.View
      style={[style, styles.sharedBackdropStyle]}
      {...otherProps}
    />
  );
};

const styles = StyleSheet.create({
  sharedBackdropStyle: StyleSheet.absoluteFillObject,
});

export default AnimatedTouchableBackdropMask;


================================================
FILE: src/components/animatedTouchableBackdropMask/types.d.ts
================================================
import {
  Animated,
  GestureResponderEvent,
  OpaqueColorValue,
  TouchableOpacityProps,
  ViewProps,
} from 'react-native';

export type RegularPropsFor<ComponentType extends 'Touch' | 'View'> =
  Animated.AnimatedProps<
    ComponentType extends 'Touch' ? TouchableOpacityProps : ViewProps
  >;

export type PropsWithHandler = RegularPropsFor<'Touch'> & {
  /**
   * Determines whether backdrop mask should receive press event.\
   * Used internally for conditional rendering
   */
  isPressable: true;

  /**
   * Function to handle press event if `isPressable` is true
   */
  pressHandler: (evt: GestureResponderEvent) => void;
};

export type PropsWithoutHandler = RegularPropsFor<'View'> & {
  /**
   * Determines whether backdrop mask should receive press event.\
   * Used internally for conditional rendering
   */
  isPressable?: false;

  /**
   * Function to handle press event if `isPressable` is true
   */
  pressHandler?: never;
};

export type AnimatedTouchableBackdropMaskProps = (
  | PropsWithHandler
  | PropsWithoutHandler
) & {
  /**
   * Ripple effect color of the backdrop when touched
   */
  android_touchRippleColor?: Animated.WithAnimatedValue<
    string | OpaqueColorValue
  >;
};


================================================
FILE: src/components/backdrop/index.tsx
================================================
import React from 'react';
import { StyleSheet, View } from 'react-native';
import AnimatedTouchableBackdropMask from '../animatedTouchableBackdropMask';
import { CUSTOM_BACKDROP_POSITIONS } from '../../types.d';
import type { BackdropProps } from './types.d';

/**
 * Abstracted, polymorphic backdrop that handles custom and default backdrop
 */
const Backdrop = ({
  BackdropComponent,
  backdropPosition = CUSTOM_BACKDROP_POSITIONS.BEHIND,
  sheetOpen,
  containerHeight,
  contentContainerHeight,
  _animatedHeight,
  closeOnPress,
  rippleColor,
  pressHandler,
  animatedBackdropOpacity,
  backdropColor,
}: BackdropProps) => {
  const heightStyle = sheetOpen ? containerHeight - contentContainerHeight : 0;

  return BackdropComponent ? (
    <View
      style={
        backdropPosition === CUSTOM_BACKDROP_POSITIONS.BEHIND
          ? StyleSheet.absoluteFillObject
          : { height: heightStyle }
      }
    >
      <BackdropComponent _animatedHeight={_animatedHeight} />
    </View>
  ) : closeOnPress ? (
    <AnimatedTouchableBackdropMask
      isPressable={true}
      android_touchRippleColor={rippleColor}
      pressHandler={pressHandler}
      style={{
        opacity: animatedBackdropOpacity,
        backgroundColor: backdropColor,
      }}
      touchSoundDisabled
      key={'TouchableBackdropMask'}
    />
  ) : (
    <AnimatedTouchableBackdropMask
      isPressable={false}
      style={{
        opacity: animatedBackdropOpacity,
        backgroundColor: backdropColor,
      }}
      key={'TouchableBackdropMask'}
    />
  );
};
export default Backdrop;


================================================
FILE: src/components/backdrop/types.d.ts
================================================
import React from 'react';
import {
  Animated,
  GestureResponderEvent,
  OpaqueColorValue,
} from 'react-native';
import { CUSTOM_BACKDROP_POSITIONS } from '../bottomSheet/types.d';

export type Color =
  | string
  | Animated.Value
  | Animated.AnimatedInterpolation<string | number>
  | OpaqueColorValue
  | undefined;

export type BackdropProps = {
  BackdropComponent?: React.FunctionComponent<{
    _animatedHeight: Animated.Value;
  }>;
  backdropPosition?:
    | CUSTOM_BACKDROP_POSITIONS
    | Lowercase<keyof typeof CUSTOM_BACKDROP_POSITIONS>;
  sheetOpen: boolean;
  containerHeight: number;
  contentContainerHeight: number;
  _animatedHeight: Animated.Value;
  closeOnPress: boolean;
  rippleColor: Color;
  pressHandler: (evt: GestureResponderEvent) => void;
  animatedBackdropOpacity: Animated.Value;
  backdropColor: Color;
};


================================================
FILE: src/components/bottomSheet/index.tsx
================================================
import React, {
  forwardRef,
  useCallback,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
  useLayoutEffect,
  useEffect,
} from 'react';
import {
  Animated,
  View,
  PanResponder,
  StyleSheet,
  type LayoutChangeEvent,
  useWindowDimensions,
  Keyboard,
  Platform,
} from 'react-native';
import {
  DEFAULT_ANIMATION,
  DEFAULT_BACKDROP_MASK_COLOR,
  DEFAULT_CLOSE_ANIMATION_DURATION,
  DEFAULT_HEIGHT,
  DEFAULT_OPEN_ANIMATION_DURATION,
} from '../../constant';
import DefaultHandleBar from '../defaultHandleBar';
import Container from '../container';
import normalizeHeight from '../../utils/normalizeHeight';
import convertHeight from '../../utils/convertHeight';
import useHandleKeyboardEvents from '../../hooks/useHandleKeyboardEvents';
import useAnimatedValue from '../../hooks/useAnimatedValue';
import Backdrop from '../backdrop';
import {
  type BottomSheetProps,
  type ToValue,
  ANIMATIONS,
  type BottomSheetMethods,
  CUSTOM_BACKDROP_POSITIONS,
  type BOTTOMSHEET,
} from './types.d';
import useHandleAndroidBackButtonClose from '../../hooks/useHandleAndroidBackButtonClose';
import separatePaddingStyles from '../../utils/separatePaddingStyles';

/**
 * Main bottom sheet component
 */
const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
  (
    {
      backdropMaskColor = DEFAULT_BACKDROP_MASK_COLOR,
      children: Children,
      animationType = DEFAULT_ANIMATION,
      closeOnBackdropPress = true,
      height = DEFAULT_HEIGHT,
      hideDragHandle = false,
      android_backdropMaskRippleColor,
      dragHandleStyle,
      disableBodyPanning = false,
      disableDragHandlePanning = false,
      customDragHandleComponent,
      style: contentContainerStyle,
      closeOnDragDown = true,
      containerHeight: passedContainerHeight,
      customBackdropComponent: CustomBackdropComponent,
      customBackdropPosition = CUSTOM_BACKDROP_POSITIONS.BEHIND,
      modal = true,
      openDuration = DEFAULT_OPEN_ANIMATION_DURATION,
      closeDuration = DEFAULT_CLOSE_ANIMATION_DURATION,
      customEasingFunction,
      android_closeOnBackPress = true,
      onClose,
      onOpen,
      onAnimate,
      disableKeyboardHandling = false,
    },
    ref
  ) => {
    /**
     * ref instance callable methods
     */
    useImperativeHandle(ref, () => ({
      open() {
        openBottomSheet();
      },
      close() {
        closeBottomSheet();
      },
    }));

    /**
     * If passed container height is a valid number we use that as final container height
     * else, it may be a percentage value so then we need to change it to a number (so it can be animated).
     * The change is handled with `onLayout` further down
     */
    const SCREEN_HEIGHT = useWindowDimensions().height; // actual container height is measured after layout
    const [containerHeight, setContainerHeight] = useState(SCREEN_HEIGHT);
    const [sheetOpen, setSheetOpen] = useState(false);

    // animated properties
    const _animatedContainerHeight = useAnimatedValue(0);
    const _animatedBackdropMaskOpacity = useAnimatedValue(0);
    const _animatedHeight = useAnimatedValue(0);

    const contentWrapperRef = useRef<View>(null);

    /** cached _nativeTag property of content container */
    const cachedContentWrapperNativeTag = useRef<number | undefined>(undefined);

    // here we separate all padding that may be applied via contentContainerStyle prop,
    // these paddings will be applied to the `View` diretly wrapping `ChildNodes` in content container.
    // All these is so that paddings applied to sheet doesn't affect the drag handle
    // TODO: find better way to memoize `separatePaddingStyles` function return value to avoid
    // redundant re-runs
    const sepStyles = useMemo(
      () => separatePaddingStyles(contentContainerStyle),
      [contentContainerStyle]
    );

    // Animation utility
    const Animators = useMemo(
      () => ({
        _slideEasingFn(value: number) {
          return value === 1 ? 1 : 1 - Math.pow(2, -10 * value);
        },
        _springEasingFn(value: number) {
          const c4 = (2 * Math.PI) / 2.5;
          return value === 0
            ? 0
            : value === 1
            ? 1
            : Math.pow(2, -9 * value) * Math.sin((value * 4.5 - 0.75) * c4) + 1;
        },
        animateContainerHeight(toValue: ToValue, duration: number = 0) {
          return Animated.timing(_animatedContainerHeight, {
            toValue: toValue,
            useNativeDriver: false,
            duration: duration,
          });
        },
        animateBackdropMaskOpacity(toValue: ToValue, duration: number) {
          // we use passed open and close durations when animation type is fade
          // but we use half of that for other animation types for good UX
          const _duration =
            animationType === ANIMATIONS.FADE ? duration : duration / 2.5;

          return Animated.timing(_animatedBackdropMaskOpacity, {
            toValue: toValue,
            useNativeDriver: false,
            duration: _duration,
          });
        },
        animateHeight(toValue: ToValue, duration: number) {
          return Animated.timing(_animatedHeight, {
            toValue,
            useNativeDriver: false,
            duration: duration,
            easing:
              customEasingFunction && typeof customEasingFunction === 'function'
                ? customEasingFunction
                : animationType === ANIMATIONS.SLIDE
                ? this._slideEasingFn
                : this._springEasingFn,
          });
        },
      }),
      [
        animationType,
        customEasingFunction,
        _animatedContainerHeight,
        _animatedBackdropMaskOpacity,
        _animatedHeight,
      ]
    );

    const interpolatedOpacity = useMemo(
      () =>
        animationType === ANIMATIONS.FADE
          ? _animatedBackdropMaskOpacity.interpolate({
              inputRange: [0, 0.5, 1],
              outputRange: [0, 0.3, 1],
              extrapolate: 'clamp',
            })
          : contentContainerStyle?.opacity,
      [animationType, contentContainerStyle, _animatedBackdropMaskOpacity]
    );

    /**
     * `height` prop converted from percentage e.g `'50%'` to pixel unit e.g `320`,
     * relative to `containerHeight` or `DEVICE_SCREEN_HEIGHT`.
     * Also auto calculates and adjusts container wrapper height when `containerHeight`
     * or `height` changes
     */
    const convertedHeight = useMemo(() => {
      const newHeight = convertHeight(height, containerHeight, hideDragHandle);

      // FIXME: we use interface-undefined but existing property `_value` here and it's risky
      // @ts-expect-error
      const curHeight = _animatedHeight._value;
      if (sheetOpen && newHeight !== curHeight) {
        if (animationType === ANIMATIONS.FADE)
          _animatedHeight.setValue(newHeight);
        else
          Animators.animateHeight(
            newHeight,
            newHeight > curHeight ? openDuration : closeDuration
          ).start();
      }
      return newHeight;
    }, [
      containerHeight,
      height,
      animationType,
      sheetOpen,
      Animators,
      _animatedHeight,
      closeDuration,
      hideDragHandle,
      openDuration,
    ]);

    /**
     * If `disableKeyboardHandling` is false, handles keyboard pop up for both platforms,
     * by auto adjusting sheet layout accordingly
     */
    const keyboardHandler = useHandleKeyboardEvents(
      !disableKeyboardHandling,
      convertedHeight,
      sheetOpen,
      Animators.animateHeight,
      contentWrapperRef
    );

    /**
     * Returns conditioned gesture handlers for content container and handle bar elements
     */
    const panHandlersFor = (view: 'handlebar' | 'contentwrapper') => {
      if (view === 'handlebar' && disableDragHandlePanning) return null;
      if (view === 'contentwrapper' && disableBodyPanning) return null;
      return PanResponder.create({
        onMoveShouldSetPanResponder: (evt) => {
          /**
           * `FiberNode._nativeTag` is stable across renders so we use it to determine
           * whether content container or it's child should respond to touch move gesture.
           *
           * The logic is, when content container is laid out, we extract it's _nativeTag property and cache it
           * So later when a move gesture event occurs within it, we compare the cached _nativeTag with the _nativeTag of
           * the event target's _nativeTag, if they match, then content container should respond, else its children should.
           * Also, when the target is the handle bar, we le it handle geture unless panning is disabled through props
           */
          return view === 'handlebar'
            ? true
            : cachedContentWrapperNativeTag.current ===
                // @ts-expect-error
                evt?.target?._nativeTag;
        },
        onPanResponderMove: (_, gestureState) => {
          if (gestureState.dy > 0) {
            // backdrop opacity relative to the height of the content sheet
            // to makes the backdrop more transparent as you drag the content sheet down
            const relativeOpacity = 1 - gestureState.dy / convertedHeight;
            _animatedBackdropMaskOpacity.setValue(relativeOpacity);

            if (animationType !== ANIMATIONS.FADE)
              _animatedHeight.setValue(convertedHeight - gestureState.dy);
          }
        },
        onPanResponderRelease(_, gestureState) {
          if (gestureState.dy >= convertedHeight / 3 && closeOnDragDown) {
            closeBottomSheet();
          } else {
            _animatedBackdropMaskOpacity.setValue(1);
            if (animationType !== ANIMATIONS.FADE)
              Animators.animateHeight(
                convertedHeight,
                openDuration / 2
              ).start();
          }
        },
      }).panHandlers;
    };

    /**
     * Polymorphic content container handle bar component
     */
    /* eslint-disable react/no-unstable-nested-components, react-native/no-inline-styles */
    const PolymorphicHandleBar: React.FunctionComponent<{}> = () => {
      const CustomHandleBar = customDragHandleComponent;
      return hideDragHandle ? null : CustomHandleBar &&
        typeof CustomHandleBar === 'function' ? (
        <View style={{ alignSelf: 'center' }} {...panHandlersFor('handlebar')}>
          <CustomHandleBar _animatedHeight={_animatedHeight} />
        </View>
      ) : (
        <DefaultHandleBar
          style={dragHandleStyle}
          {...panHandlersFor('handlebar')}
        />
      );
    };
    /* eslint-enable react/no-unstable-nested-components, react-native/no-inline-styles */

    /**
     * Extracts and caches the _nativeTag property of ContentWrapper
     */
    let extractNativeTag = useCallback(({ target }: LayoutChangeEvent) => {
      const tag =
        Platform.OS === 'web'
          ? undefined
          : // @ts-expect-error
            target?._nativeTag;
      if (!cachedContentWrapperNativeTag.current)
        cachedContentWrapperNativeTag.current = tag;
    }, []);

    /**
     * Expands the bottom sheet.
     */
    const openBottomSheet = () => {
      // 1. open container
      // 2. if using fade animation, set content container height convertedHeight manually, animate backdrop.
      // else, animate backdrop and content container height in parallel
      Animators.animateContainerHeight(
        !modal ? convertedHeight : containerHeight
      ).start();
      if (animationType === ANIMATIONS.FADE) {
        _animatedHeight.setValue(convertedHeight);
        Animators.animateBackdropMaskOpacity(1, openDuration).start();
      } else {
        Animators.animateBackdropMaskOpacity(1, openDuration).start();
        Animators.animateHeight(convertedHeight, openDuration).start();
      }
      setSheetOpen(true);

      if (onOpen) {
        onOpen();
      }
    };

    const closeBottomSheet = () => {
      // 1. fade backdrop
      // 2. if using fade animation, close container, set content wrapper height to 0.
      // else animate content container height & container height to 0, in sequence
      Animators.animateBackdropMaskOpacity(0, closeDuration).start((anim) => {
        if (anim.finished) {
          if (animationType === ANIMATIONS.FADE) {
            Animators.animateContainerHeight(0).start();
            _animatedHeight.setValue(0);
          } else {
            Animators.animateHeight(0, closeDuration).start();
            Animators.animateContainerHeight(0).start();
          }
        }
      });
      setSheetOpen(false);
      keyboardHandler?.removeKeyboardListeners();
      Keyboard.dismiss();

      if (onClose) {
        onClose();
      }
    };

    const containerViewLayoutHandler = (event: LayoutChangeEvent) => {
      const newHeight = event.nativeEvent.layout.height;
      setContainerHeight(newHeight);
      // incase `containerHeight` prop value changes when bottom sheet is expanded
      // we need to manually update the container height
      if (sheetOpen) _animatedContainerHeight.setValue(newHeight);
    };

    /**
     * Implementation logic for `onAnimate` prop
     */
    useEffect(() => {
      if (onAnimate && typeof onAnimate === 'function') {
        const animate = (
          state: Parameters<Animated.ValueListenerCallback>['0']
        ) => onAnimate(state.value);
        let listenerId: string;
        if (animationType === 'fade')
          listenerId = _animatedBackdropMaskOpacity.addListener(animate);
        else listenerId = _animatedHeight.addListener(animate);

        return () => {
          if (animationType === 'fade')
            _animatedBackdropMaskOpacity.removeListener(listenerId);
          else _animatedHeight.removeListener(listenerId);
        };
      }

      return;
    }, [
      onAnimate,
      animationType,
      _animatedBackdropMaskOpacity,
      _animatedHeight,
    ]);

    /**
     * Handles auto adjusting container view height and clamping
     * and normalizing `containerHeight` prop upon change, if its a number.
     * Also auto adjusts when orientation changes
     */
    useLayoutEffect(() => {
      if (!modal) return; // no auto layout adjustment when backdrop is hidden
      else {
        if (typeof passedContainerHeight === 'number') {
          setContainerHeight(normalizeHeight(passedContainerHeight));
          if (sheetOpen)
            _animatedContainerHeight.setValue(passedContainerHeight);
        } else if (
          typeof passedContainerHeight === 'undefined' &&
          containerHeight !== SCREEN_HEIGHT
        ) {
          setContainerHeight(SCREEN_HEIGHT);
          if (sheetOpen) _animatedContainerHeight.setValue(SCREEN_HEIGHT);
        }
      }
    }, [
      passedContainerHeight,
      SCREEN_HEIGHT,
      sheetOpen,
      containerHeight,
      modal,
      _animatedContainerHeight,
    ]);

    /**
     * Handles hardware back button press for android
     */
    useHandleAndroidBackButtonClose(
      android_closeOnBackPress,
      closeBottomSheet,
      sheetOpen
    );

    // Children
    const ChildNodes =
      typeof Children === 'function' ? (
        <Children _animatedHeight={_animatedHeight} />
      ) : (
        Children
      );

    return (
      <>
        {typeof passedContainerHeight === 'string' ? (
          /**
           * Below View handles converting `passedContainerHeight` from string to a number (to be animatable).
           * It does this by taking the string height passed via `containerHeight` prop,
           * and returning it's numeric equivalent after rendering, via its `onLayout` so we can
           * use that as the final container height.
           */
          <View
            onLayout={containerViewLayoutHandler}
            style={{
              height: passedContainerHeight,
            }}
          />
        ) : null}

        {/* Container */}
        <Container style={{ height: _animatedContainerHeight }}>
          {/* Backdrop */}
          {modal ? (
            <Backdrop
              BackdropComponent={CustomBackdropComponent}
              _animatedHeight={_animatedHeight}
              animatedBackdropOpacity={_animatedBackdropMaskOpacity}
              backdropColor={backdropMaskColor}
              backdropPosition={customBackdropPosition}
              closeOnPress={closeOnBackdropPress}
              containerHeight={containerHeight}
              contentContainerHeight={convertedHeight}
              pressHandler={closeBottomSheet}
              rippleColor={android_backdropMaskRippleColor}
              sheetOpen={sheetOpen}
            />
          ) : null}
          {/* content container */}
          <Animated.View
            ref={contentWrapperRef}
            key={'BottomSheetContentContainer'}
            onLayout={extractNativeTag}
            /* Merge external and internal styles carefully and orderly */
            style={[
              !modal ? materialStyles.contentContainerShadow : false,
              materialStyles.contentContainer,
              // we apply styles other than padding here
              sepStyles?.otherStyles,
              {
                height: _animatedHeight,
                minHeight: _animatedHeight,
                opacity: interpolatedOpacity,
              },
            ]}
            {...panHandlersFor('contentwrapper')}
          >
            <PolymorphicHandleBar />

            <View
              // we apply padding styles here to not affect drag handle above
              style={sepStyles?.paddingStyles}
            >
              {ChildNodes}
            </View>
          </Animated.View>
        </Container>
      </>
    );
  }
) as BOTTOMSHEET;

BottomSheet.displayName = 'BottomSheet';
BottomSheet.ANIMATIONS = ANIMATIONS;

const materialStyles = StyleSheet.create({
  contentContainer: {
    backgroundColor: '#F7F2FA',
    width: '100%',
    overflow: 'hidden',
    borderTopLeftRadius: 28,
    borderTopRightRadius: 28,
  },
  contentContainerShadow:
    Platform.OS === 'android'
      ? {
          elevation: 7,
        }
      : {
          shadowColor: '#000',
          shadowOffset: {
            width: 0,
            height: 3,
          },
          shadowOpacity: 0.29,
          shadowRadius: 4.65,
        },
});

export default BottomSheet;


================================================
FILE: src/components/bottomSheet/types.d.ts
================================================
import { Animated, OpaqueColorValue, ViewProps, ViewStyle } from 'react-native';
import {
  ANIMATIONS,
  CUSTOM_BACKDROP_POSITIONS,
  type BottomSheetMethods,
} from '../../types.d';
import React from 'react';

export type SheetStyleProp = Omit<
  ViewStyle,
  'height' | 'minHeight' | 'maxHeight'
>;

// short hand for toValue key of Animator methods
type ToValue = Animated.TimingAnimationConfig['toValue'];

// this is to accomodate static `ANIMATIONS` property of BottomSheet function below
type BOTTOMSHEET = React.ForwardRefExoticComponent<
  BottomSheetProps & React.RefAttributes<BottomSheetMethods>
> & { ANIMATIONS: typeof ANIMATIONS };

type AnimationType = ANIMATIONS | Lowercase<keyof typeof ANIMATIONS>;

type AnimationEasingFunction = (x: number) => number;

/**
 * Props types for bottom sheet component
 */
interface BottomSheetProps {
  /**
   * Height of the bottom sheet when expanded. This value will be relative to `containerHeight`
   * if it's supplied, or the screen's height otherwise.
   * Value can be in pixel units (number) or percentage (string).
   *
   * `Default: '50%'`
   *
   * @type {number | string}
   * @default '50%'
   * @example
   * height={300}
   * // or
   * height={'50%'}
   *
   */
  height?: number | string;

  /**
   * Extra styles to apply to bottom sheet (the `View` that wraps its children).
   *
   * `Note:` style properties `height`, `maxHeight`, `minHeight` will be ignored.
   * If you want to set sheet's height, pass the `height` prop instead.
   * @type {SheetStyleProp}
   */
  style?: SheetStyleProp;

  /**
   * Height of the bottom sheet's overall container, this will be the height of
   * the entire bottom sheet including the backdrop mask. height passed through the `height` prop
   * will be relative to this.
   *
   * `Note:` By default this will be the height of the device's screen.
   *
   * `Default: DEVICE'S SCREEN HEIGHT`
   * @type {number | string}
   * @default {DEVICE SCREEN HEIGHT}
   */
  containerHeight?: ViewStyle['height'];

  /**
   * Animation to use when opening and closing the bottom sheet.
   * Use exported `ANIMATIONS` enum to pass value to this prop
   *
   * `Default: 'slide'`
   * @type {AnimationType | ANIMATIONS}
   * @default "slide" | ANIMATIONS.SLIDE
   * @example
   * ```tsx
   * import BottomSheet, {ANIMATIONS} from '@devvie/bottom-sheet';
   * ...
   * <BottomSheet animationType={ANIMATIONS.SLIDE}>
   * ...
   * </BottomSheet>
   * ```
   */
  animationType?: AnimationType;

  /**
   * Color of the scrim or backdrop mask when `modal` is true.
   *
   * `Default: '#00000052'` (i.e black with 32% opacity)
   * @type {string | OpaqueColorValue}
   * @default '#00000052'
   */
  backdropMaskColor?: string | OpaqueColorValue;

  /**
   * Determines whether the bottom sheet will close when the scrim or backdrop mask is pressed.
   *
   * `Default: true`
   * @type {boolean}
   * @default true
   */
  closeOnBackdropPress?: boolean;

  /**
   * Determines whether bottom sheet will close when its dragged down
   * below 1/3 (one quater) of its height.
   *
   * `Default:true`
   * @type boolean
   * @default true
   */
  closeOnDragDown?: boolean;

  /**
   * When true, hides the sheet's drag handle. The drag handle is visible by default.
   *
   * `Note:` When true, custom drag handle component will also be hidden.
   *
   * `Default: false`
   *
   * @type {boolean}
   * @default false
   */
  hideDragHandle?: boolean;

  /**
       * Custom drag handle component to replace the default bottom sheet's drag handle.
       *
       * This component will be passed the animated `height` and `translateY` values of the bottom sheet,
       * which can be used to interpolate or extended animations to its children.
       *
       * `Note:` Styles passed through `dragHandleStyle` prop won't be applied to it.
       *
       * @type {React.FC<{animatedHeight: Animated.Value, animatedYTranslation:Animated.Value}>}
       * @example
       * ```tsx
       * // Below example will animate the custom drag handle's width as the 
       * // bottom sheet is being dragged/panned down
       * <BottomSheet
            customDragHandleComponent={(props) => (
              <Animated.View
              style={{
                  height: 5,
                  backgroundColor: 'orange',
                  width: props._animatedYTranslation.interpolate({
                    inputRange: [0, 25, 200],
                    outputRange: [20, 50, 100],
                  }),
                }}
              />
            )}>
            ...
          </BottomSheet>
       * ```
       */
  customDragHandleComponent?: React.FC<{
    /**
     * Animated height of the bottom sheet when expanding
     * @type {Animated.Value}
     */
    _animatedHeight: Animated.Value;
  }>;

  /**
   * Extra styles to apply to the drag handle.
   *
   * `Note:` These styles will be ignored when `customDragHandleComponent` is provided
   * @type {ViewStyle}
   */
  dragHandleStyle?: ViewStyle;

  /**
   * When true, prevents the bottom sheet from being panned down by dragging its drag handle.
   * This prop also applies to custom drag handle component provided via `customDragHandleComponent` prop.
   *
   * The bottom sheet is draggable by it's drag handle by default
   *
   * `Default:false`;
   * @type {boolean}
   * @default false
   */
  disableDragHandlePanning?: boolean;

  /**
   * When true, prevents the bottom sheet from being dragged/panned down on its body.
   * The bottom sheet body is draggable by default.
   *
   * `Default:false`;
   * @type boolean
   * @default false
   */
  disableBodyPanning?: boolean;

  /**
   * When `closeOnBackdropPress` is `true`, color of the ripple effect that occurs when scrim or backdrop mask is pressed.
   *
   * `Note:` Only works for Android
   *
   * `Default: none` (i.e no ripple effect);
   * @platform Android
   * @type (string | OpaqueColorValue)
   * @default undefined
   */
  android_backdropMaskRippleColor?: string | OpaqueColorValue;

  /**
   * Custom component for sheet's scrim or backdrop mask.\
   * `Note:` The component will be passed animated height value
   *
   * @type {React.Component}
   * @default null
   */
  customBackdropComponent?: React.FunctionComponent<{
    _animatedHeight: Animated.Value;
  }>;

  /**
   * When `customBackdropComponent` is provided, determines its position within the sheet.
   *
   * **'top'** - positions the custom backdrop component directly above the sheet\
   * **'behind'** - positions the custom backdrop component behind the sheet.
   * This is the default behaviour
   *
   * `Note:` This prop only applies to custom backdrop component
   *
   * `Default: 'behind'`
   * @type {"top" | "behind" | CUSTOM_BACKDROP_POSITIONS}
   * @default "behind" / CUSTOM_BACKDROP_POSITIONS.BEHIND
   */
  customBackdropPosition?:
    | CUSTOM_BACKDROP_POSITIONS
    | Lowercase<keyof typeof CUSTOM_BACKDROP_POSITIONS>;

  /**
   * Determines whether sheet is a modal.
   *
   * A modal sheet has a scrim or backdrop mask, while a standard (non-modal) sheet doesn't.
   *
   * `Note:` When false, this will also hide custom scrim/backdrop component supplied via `customBackdropComponent` prop.
   *
   * `Default: true`
   * @type boolean
   * @default true
   */
  modal?: boolean;

  /**
   * Contents of the bottom sheet. Can be element(s) or a component.
   *
   * If this is a component, it will be passed animated
   * height of bottom sheet via `_animatedHeight` prop
   *
   * `Default: null`
   * @type {ViewProps['children'] | React.FunctionComponent<{_animatedHeight: Animated.Value}>}
   * @default null
   */
  children:
    | ViewProps['children']
    | React.FunctionComponent<{ _animatedHeight: Animated.Value }>;

  /**
   * Duration for sheet opening animation.
   *
   * `Default: 500`
   * @type number
   * @default 500
   */
  openDuration?: number;

  /**
   * Duration for sheet closing animation.
   *
   * `Default: 500`
   * @type number
   * @default 500
   */
  closeDuration?: number;

  /**
   * Custom easing function for driving sheet's animation.\
   * If provided, easing function for passed `animationType` will be replaced with this,
   * rendering `animationType` prop obsolete
   *
   * `Default: ANIMATIONS.SLIDE`
   * @type {(AnimationEasingFunction)}
   * @default {ANIMATIONS.SLIDE}
   */
  customEasingFunction?: AnimationEasingFunction;

  /**
   * Determines whether sheet will close or not when device back button is pressed, on Android.
   *
   * __In a typical app screen (where back button takes user to the previous screen):__ \
   * _If `true` and sheet is open, the user has to press the back button twice to exit the screen.
   * The first press closes the sheet and the second exits the screen.\
   * If `false` and sheet is open, the user has to close the sheet through another means like backdrop press
   * and then press the back button after that to exit the screen_
   *
   * `Note:` Only applies for Android
   *
   * `Default: true`
   * @platform Android
   * @type {boolean}
   * @default {true}
   */
  android_closeOnBackPress?: boolean;

  /**
   * Сallback function that is called when the bottom sheet starts to close
   *
   * @type {Function}
   * @default undefined
   * @example
   * ```tsx
   * <BottomSheet
   *   onClose={() => {
   *     console.log('Bottom Sheet closing.');
   *   }}
   * />
   * ```
   */
  onClose?: Function;

  /**
   * Сallback function that is called when the bottom sheet starts to open
   *
   * @type {Function}
   * @default undefined
   * @example
   * ```tsx
   * <BottomSheet
   *   onOpen={() => {
   *     console.log('Bottom Sheet opening.');
   *   }}
   * />
   * ```
   */
  onOpen?: Function;

  /**
   * Function called with an animated number indicating the
   * progress of the opening/closing animation when sheet is being opened or closed.
   *
   * When `animationType` prop is __not__ set to `ANIMATION.FADE` or `'fade'`,
   * the number passed to this function will go from _`0`_ to _`sheet's height`_ (when sheet's opening)
   * or _`sheet's height`_ to  _`0`_ (when sheet's closing).
   *
   * When `animationType` prop is set to `ANIMATION.FADE` or `'fade'`,
   * the number will go from `0.0` to `1.0` (when sheet's opening)
   * or _`1.0`_ to  _`0.0`_ (when sheet's closing).
   *
   * `Default: undefined`
   * @default {undefined}
   * @type {(animatedHeightOrOpacity: number) => void}
   * ### Example with 'slide' or 'spring' animation
   * @example
   * ```tsx
   * <BottomSheet
       ref={sheetRef}
       height={300}
       animationType="slide"
       onAnimate={(animatedValue) => {
         console.log(animatedValue); // (0)...(150)...(300)
       }}
    >
       ...
    </BottomSheet>
   * ```
   * ### Example with 'fade' animation
   * @example 
   * ```tsx
   * <BottomSheet
       ref={sheetRef}
       height={300}
       animationType="fade"
       onAnimate={(animatedValue) => {
         console.log(animatedValue); // (0.0)...(0.5)...(1.0)
       }}
    >
       ...
    </BottomSheet>
     ```
   */
  onAnimate?: (animatedHeightOrOpacity: number) => void;

  /**
   * By default, the sheet handles keyboard pop-out automatically and smartly,
   * setting this to `true` disables that behavior, essentially leaving
   * the system to handle the keyboard the native way.
   *
   * `Default: false`
   * @type {boolean}
   * @default {false}
   */
  disableKeyboardHandling?: boolean;
}

export {
  ANIMATIONS,
  BottomSheetProps,
  CUSTOM_BACKDROP_POSITIONS,
  BottomSheetMethods,
  BOTTOMSHEET,
  AnimationEasingFunction,
  ToValue,
};


================================================
FILE: src/components/container/index.tsx
================================================
import React from 'react';
import { forwardRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native';

/**
 * This is the overall container view of the bottom sheet
 */
const Container = forwardRef<
  View,
  Animated.ComponentProps<typeof Animated.View>
>(({ style, ...otherProps }, ref) => (
  <Animated.View ref={ref} style={[styles.container, style]} {...otherProps} />
));

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    justifyContent: 'flex-end',
    right: 0,
    left: 0,
    // required to snap container to bottom of screen, so animation will start from botom to up
    bottom: 0,
    backgroundColor: 'transparent',
    opacity: 1,
  },
});

export default Container;


================================================
FILE: src/components/defaultHandleBar/index.tsx
================================================
import React from 'react';
import { StyleSheet, View } from 'react-native';
import type { DefaultHandleBarProps } from './types.d';

/**
 * This is the default handle bar component used when no custom handle bar component is provided
 */
const DefaultHandleBar = ({ style, ...otherProps }: DefaultHandleBarProps) => (
  <View style={materialStyles.dragHandleContainer} {...otherProps}>
    <View style={[materialStyles.dragHandle, style]} />
  </View>
);

const materialStyles = StyleSheet.create({
  dragHandleContainer: {
    padding: 18,
    width: 50,
    alignSelf: 'center',
  },
  dragHandle: {
    height: 4,
    width: 32,
    backgroundColor: '#49454F',
    opacity: 0.4,
    alignSelf: 'center',
    borderRadius: 50,
  },
});

// this will be used in `convertHeight` for clamping sheet height
export const DEFAULT_HANDLE_BAR_DEFAULT_HEIGHT = 25; // paddingTop (10) + paddingBottom (10) + height (5)

export default DefaultHandleBar;


================================================
FILE: src/components/defaultHandleBar/types.d.ts
================================================
import { ViewProps } from 'react-native';

export type DefaultHandleBarProps = ViewProps;


================================================
FILE: src/constant/index.ts
================================================
import { ANIMATIONS } from '../types.d';

const DEFAULT_BACKDROP_MASK_COLOR = '#00000052';
const DEFAULT_HEIGHT = '50%';
const DEFAULT_ANIMATION = ANIMATIONS.SLIDE;
const DEFAULT_OPEN_ANIMATION_DURATION = 500;
const DEFAULT_CLOSE_ANIMATION_DURATION = 500;

export {
  DEFAULT_BACKDROP_MASK_COLOR,
  DEFAULT_HEIGHT,
  DEFAULT_ANIMATION,
  DEFAULT_OPEN_ANIMATION_DURATION,
  DEFAULT_CLOSE_ANIMATION_DURATION,
};


================================================
FILE: src/hooks/useAnimatedValue.ts
================================================
import { useRef } from 'react';
import { Animated } from 'react-native';

/**
 * Convenience hook for abstracting/storing Animated values.
 * Pass your initial number value, get an animated value back.
 * @param {number} initialValue Initial animated value
 */
const useAnimatedValue = (initialValue: number = 0): Animated.Value => {
  const animatedValue = useRef(new Animated.Value(initialValue)).current;
  return animatedValue;
};

export default useAnimatedValue;


================================================
FILE: src/hooks/useHandleAndroidBackButtonClose/index.ts
================================================
import { useEffect, useRef } from 'react';
import { BackHandler, type NativeEventSubscription } from 'react-native';
import type { UseHandleAndroidBackButtonClose } from './types.d';

/**
 * Handles closing sheet when back button is pressed on
 * android and sheet is opened
 *
 * @param {boolean} shouldClose Whether to close sheet when back button is pressed
 * @param {boolean} closeSheet Function to call to close the sheet
 * @param {boolean} sheetOpen Determines the visibility of the sheet
 */
const useHandleAndroidBackButtonClose: UseHandleAndroidBackButtonClose = (
  shouldClose = true,
  closeSheet,
  sheetOpen = false
) => {
  const handler = useRef<NativeEventSubscription>();
  useEffect(() => {
    handler.current = BackHandler.addEventListener('hardwareBackPress', () => {
      if (sheetOpen) {
        if (shouldClose) {
          closeSheet?.();
        }
        return true; // prevent back press event bubbling as long as sheet is open
      } else return false; // when sheet is closed allow bubbling
    });
    return () => {
      handler.current?.remove?.();
    };
  }, [shouldClose, closeSheet, sheetOpen]);
};

export default useHandleAndroidBackButtonClose;


================================================
FILE: src/hooks/useHandleAndroidBackButtonClose/types.d.ts
================================================
export type HookReturn = void;

/**
 * Function type
 */
export type UseHandleAndroidBackButtonClose = (
  /** Whether to close sheet when back button is pressed*/
  shouldClose: boolean,
  /** Function to call to close the sheet */
  closeSheet: () => void,
  /** Determines the sheet's visibility state */
  sheetOpen: boolean
) => HookReturn;


================================================
FILE: src/hooks/useHandleKeyboardEvents/index.ts
================================================
import { useEffect, useRef } from 'react';
import {
  type EmitterSubscription,
  Keyboard,
  View,
  useWindowDimensions,
} from 'react-native';
import type { HeightAnimationDriver, UseHandleKeyboardEvents } from './types.d';

/**
 * Handles keyboard pop out by adjusting sheet's layout when a `TextInput` within
 * the sheet receives focus.
 *
 * @param {boolean} keyboardHandlingEnabled Determines whether this hook will go on to handle keyboard
 * @param {number} sheetHeight Initial sheet's height before keyboard pop out
 * @param {boolean} sheetOpen Indicates whether the sheet is open or closed
 * @param {HeightAnimationDriver} heightAnimationDriver Animator function to be called with new
 * sheet height when keyboard is out so it can adjust the sheet height with animation
 * @param {React.MutableRefObject<View>} contentWrapperRef Reference to the content wrapper view
 * @return {{removeKeyboardListeners:Function;} | null} An Object with an unsubscriber function or `null`
 *  when `keyboardHandlingEnabled` is false
 */
const useHandleKeyboardEvents: UseHandleKeyboardEvents = (
  keyboardHandlingEnabled: boolean,
  sheetHeight: number,
  sheetOpen: boolean,
  heightAnimationDriver: HeightAnimationDriver,
  contentWrapperRef: React.RefObject<View>
) => {
  const SCREEN_HEIGHT = useWindowDimensions().height;
  const keyboardHideSubscription = useRef<EmitterSubscription>();
  const keyboardShowSubscription = useRef<EmitterSubscription>();

  const unsubscribe = () => {
    keyboardHideSubscription.current?.remove?.();
    keyboardShowSubscription.current?.remove?.();
  };

  useEffect(() => {
    if (keyboardHandlingEnabled) {
      keyboardShowSubscription.current = Keyboard.addListener(
        'keyboardDidShow',
        ({ endCoordinates: { height: keyboardHeight } }) => {
          if (sheetOpen) {
            // Exaggeration of the actual height (24) of the autocorrect view
            // that appears atop android keyboard
            const keyboardAutoCorrectViewHeight = 50;
            contentWrapperRef.current?.measure?.((...result) => {
              const sheetYOffset = result[5]; // Y offset of the sheet after keyboard is out
              const actualSheetHeight = SCREEN_HEIGHT - sheetYOffset; // new height of the sheet (after keyboard offset)
              /**
               * We determine soft input/keyboard mode based on the difference between the new sheet height and the
               * initial height after keyboard is opened. If there's no much difference, it's adjustPan
               * (keyboard overlays sheet), else it's adjustResize (sheet is pushed up)
               */
              const sheetIsOverlayed =
                actualSheetHeight - sheetHeight < keyboardAutoCorrectViewHeight;
              const remainingSpace = SCREEN_HEIGHT - keyboardHeight;

              /**
               * this is 50% of the remaining space (SCREEN_HEIGHT - keyboardHeight) that remains atop the keybaord
               */
              const fiftyPercent = 0.5 * remainingSpace;
              const minSheetHeight = 50;
              // allow very short sheet e.g 10 to increase to 50 and
              // very long to clamp withing availablle space;
              let newSheetHeight = Math.max(
                minSheetHeight,
                Math.min(sheetHeight, fiftyPercent)
              );
              if (sheetIsOverlayed) newSheetHeight += keyboardHeight;
              heightAnimationDriver(newSheetHeight, 400).start();
            });
          }
        }
      );

      keyboardHideSubscription.current = Keyboard.addListener(
        'keyboardDidHide',
        (_) => {
          if (sheetOpen) heightAnimationDriver(sheetHeight, 200).start();
        }
      );
      return unsubscribe;
    }
    return;
  }, [
    keyboardHandlingEnabled,
    sheetHeight,
    SCREEN_HEIGHT,
    sheetOpen,
    heightAnimationDriver,
    contentWrapperRef,
  ]);

  return keyboardHandlingEnabled
    ? {
        removeKeyboardListeners: unsubscribe,
      }
    : null;
};

export default useHandleKeyboardEvents;


================================================
FILE: src/hooks/useHandleKeyboardEvents/types.d.ts
================================================
import React from 'react';
import { Animated, View } from 'react-native';

export type HookReturn = {
  /**
   * Removes all keyboard listeners, typically when sheet is closed
   */
  removeKeyboardListeners: () => void;
} | null;

/**
 * Handles keyboard pop out
 */
export type UseHandleKeyboardEvents = (
  /** Determines whether this hook will go on to handle keyboard */
  keyboardHandlingEnabled: boolean,
  /**
   * initial height of the sheet
   */
  sheetHeight: number,
  /** determines whether sheet is expanded */
  sheetOpen: boolean,
  /** function that can drive/animate sheet height */
  SheetHeightAnimationDriver: HeightAnimationDriver,
  /** ref to the content wrapper view for calculating sheet offset when keyboard is out */
  contentWrapperRef: React.RefObject<View>
) => HookReturn;

export type HeightAnimationDriver = (
  height: number,
  duration: number
) => Animated.CompositeAnimation;


================================================
FILE: src/index.ts
================================================
import BottomSheet from './components/bottomSheet';
export { default as AnimatedTouchableBackdropMask } from './components/animatedTouchableBackdropMask';
export {
  ANIMATIONS,
  CUSTOM_BACKDROP_POSITIONS,
  type BottomSheetMethods,
} from './types.d';
export type { BottomSheetProps } from './components/bottomSheet/types.d';
export default BottomSheet;


================================================
FILE: src/types.d.ts
================================================
/**
 * Supported animation types
 */
export enum ANIMATIONS {
  SLIDE = 'slide',
  SPRING = 'spring',
  FADE = 'fade',
}

/**
 * Alias for `ANIMATIONS` to allow literal animation type string as prop
 * @alias ANIMATIONS
 */
export type AnimationType = Lowercase<keyof typeof ANIMATIONS>;

/**
 * Supported custom backdrop component position
 */
export enum CUSTOM_BACKDROP_POSITIONS {
  TOP = 'top',
  BEHIND = 'behind',
}

/**
 * Bottom sheet's ref instance methods
 */
export interface BottomSheetMethods {
  /**
   * Expands the bottom sheet to the `height` passed through props
   */
  open(): void;
  /**
   * Collapses the bottom sheet
   */
  close(): void;
}


================================================
FILE: src/utils/convertHeight.ts
================================================
import { Dimensions } from 'react-native';
import { DEFAULT_HANDLE_BAR_DEFAULT_HEIGHT } from '../components/defaultHandleBar';

/**
 * converts string `height` from percentage e.g `'50%'` to pixel unit e.g `320` relative to `containerHeight`,
 * or also clamps it to not exceed `containerHeight` if it's a number.
 *
 * _Note: When `height` > `containerHeight` and `containerHeight` === `SCREEN_HEIGHT`, and handle
 * bar is visible, we want to set `height` to `SCREEN_HEIGHT`
 * but deducting the height of handle bar so it's still visible._
 * @param {string | number} height height in number percentage string
 * @param {number} containerHeight height to convert and clamp relative to
 * @param {boolean} handleBarHidden Used to determine how height clamping is done
 * @returns {number} converted height
 */
const convertHeight = (
  height: string | number,
  containerHeight: number,
  handleBarHidden: boolean
): number => {
  const SCREEN_HEIGHT = Dimensions.get('window').height;
  let _height = height;
  const errorMsg = 'Invalid `height` prop';
  if (typeof height === 'number') {
    // normalise height
    if (height < 0) _height = 0;
    if (height >= containerHeight) {
      if (containerHeight === SCREEN_HEIGHT && !handleBarHidden) {
        _height = containerHeight - DEFAULT_HANDLE_BAR_DEFAULT_HEIGHT;
      } else _height = containerHeight;
    }
  } else if (typeof height === 'string') {
    const lastPercentIdx = height.lastIndexOf('%');

    // perform checks
    if (!height.endsWith('%') || height.length <= 1 || height.length > 4)
      throw errorMsg;
    let parsedHeight = Math.abs(
      parseInt(height.substring(0, lastPercentIdx), 10)
    );
    if (isNaN(parsedHeight)) throw errorMsg;

    // normalise height
    if (parsedHeight >= 100) {
      parsedHeight = 100;
    }

    _height = Math.floor((parsedHeight / 100) * containerHeight);
    if (containerHeight === SCREEN_HEIGHT && !handleBarHidden) {
      _height -= DEFAULT_HANDLE_BAR_DEFAULT_HEIGHT;
    }
  } else throw errorMsg;

  return _height as number;
};

export default convertHeight;


================================================
FILE: src/utils/normalizeHeight.ts
================================================
import { Dimensions } from 'react-native';

/**
 * Normalizes height to a number and clamps it
 * so it's not bigger that device screen height
 */
const normalizeHeight = (height?: number | string): number => {
  const DEVICE_SCREEN_HEIGHT = Dimensions.get('window').height;
  let clampedHeight = DEVICE_SCREEN_HEIGHT;
  if (typeof height === 'number')
    clampedHeight =
      height < 0
        ? 0
        : height > DEVICE_SCREEN_HEIGHT
        ? DEVICE_SCREEN_HEIGHT
        : height;
  return clampedHeight;
};

export default normalizeHeight;


================================================
FILE: src/utils/separatePaddingStyles.ts
================================================
import { type ViewStyle } from 'react-native';
import type { SheetStyleProp } from '../components/bottomSheet/types';

type PadProps =
  | 'padding'
  | 'paddingBottom'
  | 'paddingEnd'
  | 'paddingHorizontal'
  | 'paddingLeft'
  | 'paddingRight'
  | 'paddingStart'
  | 'paddingTop'
  | 'paddingVertical';
type Styles = {
  paddingStyles: Pick<ViewStyle, PadProps>;
  otherStyles: Omit<ViewStyle, PadProps>;
};

/**
 * Extracts and separates `padding` styles from
 * other styles from the given `style`
 */
const separatePaddingStyles = (
  style: SheetStyleProp | undefined
): Styles | undefined => {
  if (!style) return;
  const styleKeys = Object.keys(style || {});
  if (!styleKeys.length) return;

  const styles: Styles = {
    paddingStyles: {},
    otherStyles: {},
  };

  for (const key of styleKeys) {
    // @ts-ignore
    styles[key.startsWith('padding') ? 'paddingStyles' : 'otherStyles'][key] =
      // @ts-ignore
      style[key];
  }

  return styles;
};

export default separatePaddingStyles;


================================================
FILE: tsconfig.build.json
================================================
{
  "exclude": ["example", "src/__tests__"],
  "compilerOptions": {
    "skipLibCheck": true,
    "jsx": "react",
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "outDir": "./lib/typescript",
    "pretty": true,
    "emitDeclarationOnly": true
  }
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "rootDir": ".",
    "paths": {
      "@devvie/bottom-sheet": ["./src/index"]
    },
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "jsx": "react",
    "lib": ["esnext"],
    "module": "esnext",
    "moduleResolution": "node",
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noImplicitUseStrict": false,
    "noStrictGenericChecks": false,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "esnext",
    "verbatimModuleSyntax": true
  }
}
Download .txt
gitextract_pza16rl0/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── actions/
│   │   └── setup/
│   │       └── action.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .nvmrc
├── .watchmanconfig
├── .yarnrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── babel.config.js
├── commitlint.config.ts
├── example/
│   ├── App.js
│   ├── app.json
│   ├── babel.config.js
│   ├── metro.config.js
│   ├── package.json
│   ├── src/
│   │   └── App.tsx
│   ├── tsconfig.json
│   └── webpack.config.js
├── lefthook.yml
├── package.json
├── scripts/
│   ├── bootstrap.js
│   ├── copy-dts.js
│   └── delete-lib-dir.js
├── src/
│   ├── __tests__/
│   │   └── index.test.tsx
│   ├── components/
│   │   ├── animatedTouchableBackdropMask/
│   │   │   ├── index.tsx
│   │   │   └── types.d.ts
│   │   ├── backdrop/
│   │   │   ├── index.tsx
│   │   │   └── types.d.ts
│   │   ├── bottomSheet/
│   │   │   ├── index.tsx
│   │   │   └── types.d.ts
│   │   ├── container/
│   │   │   └── index.tsx
│   │   └── defaultHandleBar/
│   │       ├── index.tsx
│   │       └── types.d.ts
│   ├── constant/
│   │   └── index.ts
│   ├── hooks/
│   │   ├── useAnimatedValue.ts
│   │   ├── useHandleAndroidBackButtonClose/
│   │   │   ├── index.ts
│   │   │   └── types.d.ts
│   │   └── useHandleKeyboardEvents/
│   │       ├── index.ts
│   │       └── types.d.ts
│   ├── index.ts
│   ├── types.d.ts
│   └── utils/
│       ├── convertHeight.ts
│       ├── normalizeHeight.ts
│       └── separatePaddingStyles.ts
├── tsconfig.build.json
└── tsconfig.json
Download .txt
SYMBOL INDEX (40 symbols across 13 files)

FILE: example/src/App.tsx
  function App (line 6) | function App() {

FILE: scripts/copy-dts.js
  function copyDeclarationFiles (line 11) | function copyDeclarationFiles(src, dest) {

FILE: src/components/animatedTouchableBackdropMask/types.d.ts
  type RegularPropsFor (line 9) | type RegularPropsFor<ComponentType extends 'Touch' | 'View'> =
  type PropsWithHandler (line 14) | type PropsWithHandler = RegularPropsFor<'Touch'> & {
  type PropsWithoutHandler (line 27) | type PropsWithoutHandler = RegularPropsFor<'View'> & {
  type AnimatedTouchableBackdropMaskProps (line 40) | type AnimatedTouchableBackdropMaskProps = (

FILE: src/components/backdrop/types.d.ts
  type Color (line 9) | type Color =
  type BackdropProps (line 16) | type BackdropProps = {

FILE: src/components/bottomSheet/index.tsx
  method open (line 84) | open() {
  method close (line 87) | close() {
  method _slideEasingFn (line 124) | _slideEasingFn(value: number) {
  method _springEasingFn (line 127) | _springEasingFn(value: number) {
  method animateContainerHeight (line 135) | animateContainerHeight(toValue: ToValue, duration: number = 0) {
  method animateBackdropMaskOpacity (line 142) | animateBackdropMaskOpacity(toValue: ToValue, duration: number) {
  method animateHeight (line 154) | animateHeight(toValue: ToValue, duration: number) {
  method onPanResponderRelease (line 269) | onPanResponderRelease(_, gestureState) {

FILE: src/components/bottomSheet/types.d.ts
  type SheetStyleProp (line 9) | type SheetStyleProp = Omit<
  type ToValue (line 15) | type ToValue = Animated.TimingAnimationConfig['toValue'];
  type BOTTOMSHEET (line 18) | type BOTTOMSHEET = React.ForwardRefExoticComponent<
  type AnimationType (line 22) | type AnimationType = ANIMATIONS | Lowercase<keyof typeof ANIMATIONS>;
  type AnimationEasingFunction (line 24) | type AnimationEasingFunction = (x: number) => number;
  type BottomSheetProps (line 29) | interface BottomSheetProps {

FILE: src/components/defaultHandleBar/index.tsx
  constant DEFAULT_HANDLE_BAR_DEFAULT_HEIGHT (line 31) | const DEFAULT_HANDLE_BAR_DEFAULT_HEIGHT = 25;

FILE: src/components/defaultHandleBar/types.d.ts
  type DefaultHandleBarProps (line 3) | type DefaultHandleBarProps = ViewProps;

FILE: src/constant/index.ts
  constant DEFAULT_BACKDROP_MASK_COLOR (line 3) | const DEFAULT_BACKDROP_MASK_COLOR = '#00000052';
  constant DEFAULT_HEIGHT (line 4) | const DEFAULT_HEIGHT = '50%';
  constant DEFAULT_ANIMATION (line 5) | const DEFAULT_ANIMATION = ANIMATIONS.SLIDE;
  constant DEFAULT_OPEN_ANIMATION_DURATION (line 6) | const DEFAULT_OPEN_ANIMATION_DURATION = 500;
  constant DEFAULT_CLOSE_ANIMATION_DURATION (line 7) | const DEFAULT_CLOSE_ANIMATION_DURATION = 500;

FILE: src/hooks/useHandleAndroidBackButtonClose/types.d.ts
  type HookReturn (line 1) | type HookReturn = void;
  type UseHandleAndroidBackButtonClose (line 6) | type UseHandleAndroidBackButtonClose = (

FILE: src/hooks/useHandleKeyboardEvents/types.d.ts
  type HookReturn (line 4) | type HookReturn = {
  type UseHandleKeyboardEvents (line 14) | type UseHandleKeyboardEvents = (
  type HeightAnimationDriver (line 29) | type HeightAnimationDriver = (

FILE: src/types.d.ts
  type ANIMATIONS (line 4) | enum ANIMATIONS {
  type AnimationType (line 14) | type AnimationType = Lowercase<keyof typeof ANIMATIONS>;
  type CUSTOM_BACKDROP_POSITIONS (line 19) | enum CUSTOM_BACKDROP_POSITIONS {
  type BottomSheetMethods (line 27) | interface BottomSheetMethods {

FILE: src/utils/separatePaddingStyles.ts
  type PadProps (line 4) | type PadProps =
  type Styles (line 14) | type Styles = {
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (97K chars).
[
  {
    "path": ".editorconfig",
    "chars": 283,
    "preview": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# edit"
  },
  {
    "path": ".gitattributes",
    "chars": 71,
    "preview": "*.pbxproj -text\n# specific for windows script files\n*.bat text eol=crlf"
  },
  {
    "path": ".github/actions/setup/action.yml",
    "chars": 779,
    "preview": "name: Setup\ndescription: Setup Node.js and install dependencies\n\nruns:\n  using: composite\n  steps:\n    - name: Setup Nod"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1095,
    "preview": "name: CI\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  lint:\n    runs-on: u"
  },
  {
    "path": ".gitignore",
    "chars": 689,
    "preview": "# OSX\n#\n.DS_Store\n\n# XDE\n.expo/\n\n# VSCode\n.vscode/\njsconfig.json\n\n# Xcode\n#\nbuild/\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n"
  },
  {
    "path": ".nvmrc",
    "chars": 4,
    "preview": "v18\n"
  },
  {
    "path": ".watchmanconfig",
    "chars": 2,
    "preview": "{}"
  },
  {
    "path": ".yarnrc",
    "chars": 115,
    "preview": "# Override Yarn command so we can automatically setup the repo on running `yarn`\n\nyarn-path \"scripts/bootstrap.js\"\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5489,
    "preview": "\n# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make particip"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 4072,
    "preview": "# Contributing\n\nContributions are always welcome, no matter how large or small!\n\nWe want this community to be friendly a"
  },
  {
    "path": "LICENSE",
    "chars": 1062,
    "preview": "MIT License\n\nCopyright (c) 2023 Devvie\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
  },
  {
    "path": "README.md",
    "chars": 14073,
    "preview": "# React Native Bottom Sheet 💖\n\n![GitHub](https://img.shields.io/github/license/stanleyugwu/react-native-bottom-sheet?sty"
  },
  {
    "path": "babel.config.js",
    "chars": 77,
    "preview": "module.exports = {\n  presets: ['module:metro-react-native-babel-preset'],\n};\n"
  },
  {
    "path": "commitlint.config.ts",
    "chars": 909,
    "preview": "import type { UserConfig } from '@commitlint/types';\n\nconst Configuration: UserConfig = {\n  extends: ['@commitlint/confi"
  },
  {
    "path": "example/App.js",
    "chars": 37,
    "preview": "export { default } from './src/App';\n"
  },
  {
    "path": "example/app.json",
    "chars": 623,
    "preview": "{\n  \"expo\": {\n    \"name\": \"example\",\n    \"slug\": \"example\",\n    \"version\": \"1.0.0\",\n    \"orientation\": \"portrait\",\n    \""
  },
  {
    "path": "example/babel.config.js",
    "chars": 493,
    "preview": "const path = require('path');\nconst pak = require('../package.json');\n\nmodule.exports = function (api) {\n  api.cache(tru"
  },
  {
    "path": "example/metro.config.js",
    "chars": 1188,
    "preview": "const path = require('path');\nconst escape = require('escape-string-regexp');\nconst { getDefaultConfig } = require('@exp"
  },
  {
    "path": "example/package.json",
    "chars": 661,
    "preview": "{\n  \"name\": \"example\",\n  \"version\": \"1.0.0\",\n  \"main\": \"node_modules/expo/AppEntry.js\",\n  \"scripts\": {\n    \"start\": \"exp"
  },
  {
    "path": "example/src/App.tsx",
    "chars": 910,
    "preview": "import * as React from 'react';\nimport { StyleSheet, View, Text, Button } from 'react-native';\n// @ts-ignore ts can't fi"
  },
  {
    "path": "example/tsconfig.json",
    "chars": 108,
    "preview": "{\n  \"extends\": \"../tsconfig\",\n  \"compilerOptions\": {\n    // Avoid expo-cli auto-generating a tsconfig\n  }\n}\n"
  },
  {
    "path": "example/webpack.config.js",
    "chars": 810,
    "preview": "const path = require('path');\nconst createExpoWebpackConfigAsync = require('@expo/webpack-config');\nconst { resolver } ="
  },
  {
    "path": "lefthook.yml",
    "chars": 360,
    "preview": "pre-commit:\n  parallel: true\n  commands:\n    lint:\n      files: git diff --name-only @{push}\n      glob: \"*.{js,ts,jsx,t"
  },
  {
    "path": "package.json",
    "chars": 4402,
    "preview": "{\n  \"name\": \"@devvie/bottom-sheet\",\n  \"version\": \"0.4.3\",\n  \"description\": \"The 😎smart , 📦tiny , and 🎗flexible bottom sh"
  },
  {
    "path": "scripts/bootstrap.js",
    "chars": 769,
    "preview": "const os = require('os');\nconst path = require('path');\nconst child_process = require('child_process');\n\nconst root = pa"
  },
  {
    "path": "scripts/copy-dts.js",
    "chars": 1158,
    "preview": "const fs = require('fs-extra');\nconst path = require('path');\n\nconst sourceDirectory = './src'; // Change this to your s"
  },
  {
    "path": "scripts/delete-lib-dir.js",
    "chars": 1056,
    "preview": "const fs = require('fs');\nconst os = require('os');\nconst { exec } = require('child_process');\n\n// Define the relative d"
  },
  {
    "path": "src/__tests__/index.test.tsx",
    "chars": 25,
    "preview": "it.todo('write a test');\n"
  },
  {
    "path": "src/components/animatedTouchableBackdropMask/index.tsx",
    "chars": 1160,
    "preview": "import React from 'react';\nimport { Animated, Pressable, StyleSheet } from 'react-native';\nimport { type AnimatedTouchab"
  },
  {
    "path": "src/components/animatedTouchableBackdropMask/types.d.ts",
    "chars": 1215,
    "preview": "import {\n  Animated,\n  GestureResponderEvent,\n  OpaqueColorValue,\n  TouchableOpacityProps,\n  ViewProps,\n} from 'react-na"
  },
  {
    "path": "src/components/backdrop/index.tsx",
    "chars": 1585,
    "preview": "import React from 'react';\nimport { StyleSheet, View } from 'react-native';\nimport AnimatedTouchableBackdropMask from '."
  },
  {
    "path": "src/components/backdrop/types.d.ts",
    "chars": 844,
    "preview": "import React from 'react';\nimport {\n  Animated,\n  GestureResponderEvent,\n  OpaqueColorValue,\n} from 'react-native';\nimpo"
  },
  {
    "path": "src/components/bottomSheet/index.tsx",
    "chars": 18385,
    "preview": "import React, {\n  forwardRef,\n  useCallback,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n  useLayoutEffect,\n"
  },
  {
    "path": "src/components/bottomSheet/types.d.ts",
    "chars": 11616,
    "preview": "import { Animated, OpaqueColorValue, ViewProps, ViewStyle } from 'react-native';\nimport {\n  ANIMATIONS,\n  CUSTOM_BACKDRO"
  },
  {
    "path": "src/components/container/index.tsx",
    "chars": 733,
    "preview": "import React from 'react';\nimport { forwardRef } from 'react';\nimport { Animated, StyleSheet, View } from 'react-native'"
  },
  {
    "path": "src/components/defaultHandleBar/index.tsx",
    "chars": 945,
    "preview": "import React from 'react';\nimport { StyleSheet, View } from 'react-native';\nimport type { DefaultHandleBarProps } from '"
  },
  {
    "path": "src/components/defaultHandleBar/types.d.ts",
    "chars": 90,
    "preview": "import { ViewProps } from 'react-native';\n\nexport type DefaultHandleBarProps = ViewProps;\n"
  },
  {
    "path": "src/constant/index.ts",
    "chars": 410,
    "preview": "import { ANIMATIONS } from '../types.d';\n\nconst DEFAULT_BACKDROP_MASK_COLOR = '#00000052';\nconst DEFAULT_HEIGHT = '50%';"
  },
  {
    "path": "src/hooks/useAnimatedValue.ts",
    "chars": 469,
    "preview": "import { useRef } from 'react';\nimport { Animated } from 'react-native';\n\n/**\n * Convenience hook for abstracting/storin"
  },
  {
    "path": "src/hooks/useHandleAndroidBackButtonClose/index.ts",
    "chars": 1192,
    "preview": "import { useEffect, useRef } from 'react';\nimport { BackHandler, type NativeEventSubscription } from 'react-native';\nimp"
  },
  {
    "path": "src/hooks/useHandleAndroidBackButtonClose/types.d.ts",
    "chars": 346,
    "preview": "export type HookReturn = void;\n\n/**\n * Function type\n */\nexport type UseHandleAndroidBackButtonClose = (\n  /** Whether t"
  },
  {
    "path": "src/hooks/useHandleKeyboardEvents/index.ts",
    "chars": 4077,
    "preview": "import { useEffect, useRef } from 'react';\nimport {\n  type EmitterSubscription,\n  Keyboard,\n  View,\n  useWindowDimension"
  },
  {
    "path": "src/hooks/useHandleKeyboardEvents/types.d.ts",
    "chars": 916,
    "preview": "import React from 'react';\nimport { Animated, View } from 'react-native';\n\nexport type HookReturn = {\n  /**\n   * Removes"
  },
  {
    "path": "src/index.ts",
    "chars": 356,
    "preview": "import BottomSheet from './components/bottomSheet';\nexport { default as AnimatedTouchableBackdropMask } from './componen"
  },
  {
    "path": "src/types.d.ts",
    "chars": 667,
    "preview": "/**\n * Supported animation types\n */\nexport enum ANIMATIONS {\n  SLIDE = 'slide',\n  SPRING = 'spring',\n  FADE = 'fade',\n}"
  },
  {
    "path": "src/utils/convertHeight.ts",
    "chars": 2092,
    "preview": "import { Dimensions } from 'react-native';\nimport { DEFAULT_HANDLE_BAR_DEFAULT_HEIGHT } from '../components/defaultHandl"
  },
  {
    "path": "src/utils/normalizeHeight.ts",
    "chars": 551,
    "preview": "import { Dimensions } from 'react-native';\n\n/**\n * Normalizes height to a number and clamps it\n * so it's not bigger tha"
  },
  {
    "path": "src/utils/separatePaddingStyles.ts",
    "chars": 1013,
    "preview": "import { type ViewStyle } from 'react-native';\nimport type { SheetStyleProp } from '../components/bottomSheet/types';\n\nt"
  },
  {
    "path": "tsconfig.build.json",
    "chars": 288,
    "preview": "{\n  \"exclude\": [\"example\", \"src/__tests__\"],\n  \"compilerOptions\": {\n    \"skipLibCheck\": true,\n    \"jsx\": \"react\",\n    \"d"
  },
  {
    "path": "tsconfig.json",
    "chars": 733,
    "preview": "{\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"paths\": {\n      \"@devvie/bottom-sheet\": [\"./src/index\"]\n    },\n    \"al"
  }
]

About this extraction

This page contains the full source code of the stanleyugwu/react-native-bottom-sheet GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (88.9 KB), approximately 22.3k tokens, and a symbol index with 40 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!