├── .github ├── FUNDING.yml ├── assets │ ├── DraggableStackExample.gif │ ├── demo.gif │ ├── logo.png │ └── logo2.png └── workflows │ ├── main.yaml │ └── pages.yaml ├── .gitignore ├── .npmrc ├── .release-it.json ├── LICENSE.md ├── README.md ├── babel.config.cjs ├── docs ├── .gitignore ├── .prettierrc ├── README.md ├── astro.config.mjs ├── package.json ├── pnpm-lock.yaml ├── public │ └── favicon.svg ├── src │ ├── components │ │ ├── Link.tsx │ │ ├── Logo.tsx │ │ ├── Search.tsx │ │ ├── icon │ │ │ ├── CloseIcon.tsx │ │ │ ├── DarkIcon.tsx │ │ │ ├── GitHubIcon.tsx │ │ │ ├── LightIcon.tsx │ │ │ ├── MenuIcon.tsx │ │ │ ├── SearchIcon.tsx │ │ │ ├── SystemIcon.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── layout │ │ │ ├── Article.tsx │ │ │ ├── Header.tsx │ │ │ ├── MobileNavigation.tsx │ │ │ ├── Navigation.tsx │ │ │ └── index.ts │ │ └── markdown │ │ │ ├── CodeBlock.astro │ │ │ ├── Properties.astro │ │ │ ├── Property.astro │ │ │ ├── components │ │ │ ├── ClipboardIcon.tsx │ │ │ └── CopyButton.tsx │ │ │ └── index.ts │ ├── config │ │ ├── env.ts │ │ └── navigation.ts │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── components │ │ │ ├── Draggable.mdx │ │ │ ├── DraggableGrid.mdx │ │ │ ├── DraggableStack.mdx │ │ │ └── Droppable.mdx │ │ │ ├── guides │ │ │ ├── data.mdx │ │ │ ├── install.mdx │ │ │ ├── introduction.mdx │ │ │ └── quickstart.mdx │ │ │ ├── hooks │ │ │ ├── useActiveDragReaction.mdx │ │ │ ├── useActiveDropReaction.mdx │ │ │ ├── useDraggable.mdx │ │ │ ├── useDraggableActiveId.mdx │ │ │ └── useDroppable.mdx │ │ │ ├── providers │ │ │ └── DndProvider.mdx │ │ │ └── utils │ │ │ ├── useEvent.mdx │ │ │ ├── useLatestValue.mdx │ │ │ └── useNodeRef.mdx │ ├── env.d.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useDidMount.ts │ │ ├── useIsScrolled.ts │ │ └── useNavigationStore.ts │ ├── layouts │ │ └── main.astro │ ├── pages │ │ ├── [...slug].astro │ │ └── index.astro │ ├── styles │ │ ├── base.css │ │ ├── main.css │ │ └── reset.css │ └── utils │ │ ├── classNames.ts │ │ ├── index.ts │ │ └── string.ts ├── tailwind.config.mjs └── tsconfig.json ├── eslint.config.js ├── example └── src │ ├── App.tsx │ └── pages │ ├── DraggableBasicExample.tsx │ ├── DraggableGridExample.tsx │ └── DraggableStackExample.tsx ├── jest.config.cjs ├── package.json ├── pnpm-lock.yaml ├── prettier.config.js ├── src ├── DndContext.ts ├── DndProvider.tsx ├── components │ ├── Draggable.tsx │ ├── Droppable.tsx │ └── index.ts ├── features │ ├── index.ts │ └── sort │ │ ├── components │ │ ├── DraggableGrid.tsx │ │ ├── DraggableStack.tsx │ │ └── index.ts │ │ ├── hooks │ │ ├── index.ts │ │ ├── useDraggableGrid.ts │ │ ├── useDraggableSort.ts │ │ └── useDraggableStack.ts │ │ └── index.ts ├── hooks │ ├── index.ts │ ├── useActiveDragReaction.ts │ ├── useActiveDropReaction.ts │ ├── useChildrenIds.ts │ ├── useDraggable.ts │ ├── useDraggableActiveId.ts │ ├── useDraggableStyle.tsx │ ├── useDroppable.ts │ ├── useDroppableStyle.tsx │ ├── useEvent.ts │ ├── useLatestSharedValue.ts │ ├── useLatestValue.ts │ ├── useNodeRef.ts │ ├── useSharedPoint.ts │ └── useSharedValuePair.ts ├── index.ts ├── types │ ├── common.ts │ ├── index.ts │ └── reanimated.ts └── utils │ ├── array.ts │ ├── assert.ts │ ├── collision.ts │ ├── geometry.ts │ ├── index.ts │ ├── random.ts │ └── reanimated.ts ├── test ├── DndProvider.spec.tsx └── setup.ts ├── tsconfig.build.json └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mgcrea] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/assets/DraggableStackExample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgcrea/react-native-dnd/adb3c539214c1b5d03c832a9a5000e565abc1b27/.github/assets/DraggableStackExample.gif -------------------------------------------------------------------------------- /.github/assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgcrea/react-native-dnd/adb3c539214c1b5d03c832a9a5000e565abc1b27/.github/assets/demo.gif -------------------------------------------------------------------------------- /.github/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgcrea/react-native-dnd/adb3c539214c1b5d03c832a9a5000e565abc1b27/.github/assets/logo.png -------------------------------------------------------------------------------- /.github/assets/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgcrea/react-native-dnd/adb3c539214c1b5d03c832a9a5000e565abc1b27/.github/assets/logo2.png -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node: ["lts/-1", "lts/*"] 16 | name: Test on node@v${{ matrix.node }} 17 | steps: 18 | - name: Checkout 🛎️ 19 | uses: actions/checkout@v4 20 | - name: Setup pnpm 🔧 21 | uses: pnpm/action-setup@v3 22 | with: 23 | version: 9 24 | - name: Setup node 🔧 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node }} 28 | check-latest: true 29 | cache: "pnpm" 30 | - name: Install 🪄 31 | run: pnpm install --frozen-lockfile 32 | - name: Lint 🔍 33 | run: pnpm run lint 34 | - name: TypeScript 🔍 35 | run: pnpm run check 36 | - name: Vitest 🔍 37 | run: pnpm run spec 38 | build: 39 | runs-on: ubuntu-latest 40 | strategy: 41 | matrix: 42 | node: ["lts/-1", "lts/*"] 43 | name: Build on node@v${{ matrix.node }} 44 | steps: 45 | - name: Checkout 🛎️ 46 | uses: actions/checkout@v4 47 | - name: Setup pnpm 🔧 48 | uses: pnpm/action-setup@v3 49 | with: 50 | version: 9 51 | - name: Setup node 🔧 52 | uses: actions/setup-node@v4 53 | with: 54 | node-version: ${{ matrix.node }} 55 | check-latest: true 56 | cache: "pnpm" 57 | - name: Install 🪄 58 | run: pnpm install --frozen-lockfile 59 | - name: Build 💎 60 | run: pnpm run build 61 | -------------------------------------------------------------------------------- /.github/workflows/pages.yaml: -------------------------------------------------------------------------------- 1 | name: pages 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["master"] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: false 22 | 23 | defaults: 24 | run: 25 | working-directory: docs 26 | 27 | jobs: 28 | build: 29 | runs-on: ubuntu-latest 30 | name: Build Github Pages 31 | steps: 32 | - name: Checkout 🛎️ 33 | uses: actions/checkout@v3 34 | # - name: Install, build, and upload your site 35 | # uses: withastro/action@v0 36 | # with: 37 | # path: docs 38 | # package-manager: pnpm 39 | - name: Setup pnpm 🔧 40 | uses: pnpm/action-setup@v2 41 | with: 42 | version: 8 43 | - name: Setup node 🔧 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: lts/* 47 | check-latest: true 48 | cache: "pnpm" 49 | - name: Configure 🔧 50 | uses: actions/configure-pages@v3 51 | - name: Install 🪄 52 | run: pnpm install --frozen-lockfile 53 | - name: Build 💎 54 | run: pnpm run build 55 | - name: Upload 🚀 56 | uses: actions/upload-pages-artifact@v1 57 | with: 58 | path: "docs/dist" 59 | 60 | deploy: 61 | needs: build 62 | runs-on: ubuntu-latest 63 | name: Deploy Github Pages 64 | environment: 65 | name: github-pages 66 | url: ${{ steps.deployment.outputs.page_url }} 67 | steps: 68 | - name: Deploy 🚀 69 | id: deployment 70 | uses: actions/deploy-pages@v2 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | dist-alt/ 4 | 5 | # generated types 6 | .astro/ 7 | 8 | # dependencies 9 | node_modules/ 10 | 11 | # logs 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # environment variables 18 | .env 19 | .env.production 20 | 21 | # macOS-specific files 22 | .DS_Store 23 | 24 | # others 25 | .idea/ 26 | .vscode/ 27 | example/ 28 | expo/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore(release): cut the v${version} release", 4 | "tagAnnotation": "v${version} release" 5 | }, 6 | "github": { 7 | "release": true, 8 | "releaseName": "v${version}" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (c) 2023 Olivier Louvignes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | logo 5 | 6 |

7 |

8 | 9 | npm version 10 | 11 | 12 | npm total downloads 13 | 14 | 15 | npm monthly downloads 16 | 17 | 18 | npm license 19 | 20 |
21 | 22 | build status 23 | 24 | 25 | dependencies status 26 | 27 |

28 | 29 | 30 | ## Features 31 | 32 | Modern and easy-to-use drag&drop library for react-native. 33 | 34 | - **Modern and future-proof:** Built on [react-native-reanimated v3](https://github.com/software-mansion/react-native-reanimated/) 35 | 36 | - **Hooks based:** Exposes powerful hooks [`useDraggable`](./src/hooks/useDraggable.ts) and [`useDroppable`](./src/hooks/useDroppable.ts) to build your own primitive components. 37 | 38 | - **Ready to use:** Out-of-the-box configurable components like [`Draggable`](./src/components/Draggable.tsx) and [`Droppable`](./src/components/Droppable.tsx) to quickly get started. 39 | 40 | - **Powerful & Performant:** The implementation has been tailored for advanced use cases where performance is critical. It can be used for a wide range of use cases like sorting items. 41 | 42 | Check the [**Documentation**](https://mgcrea.github.io/react-native-dnd/) for usage details. 43 | 44 | ## Sponsors 45 | 46 | If you need a specific feature or want to speed up the development of this library, you can sponsor me on [GitHub Sponsors](https://github.com/sponsors/mgcrea). Feel free to reach out to me if you have any questions. 47 | 48 | ## Demo 49 | 50 | ![demo](./.github/assets/demo.gif) 51 | 52 | A working project can be found at [react-native-dnd-demo](https://github.com/mgcrea/react-native-dnd-demo) 53 | 54 | ## Install 55 | 56 | ```bash 57 | npm install @mgcrea/react-native-dnd --save 58 | # or 59 | yarn add @mgcrea/react-native-dnd 60 | # or 61 | pnpm add @mgcrea/react-native-dnd 62 | ``` 63 | 64 | ### Dependencies 65 | 66 | This package relies on : 67 | 68 | - [react-native-reanimated@>=3](https://github.com/software-mansion/react-native-reanimated/) 69 | - [react-native-gesture-handler@>=2](https://github.com/software-mansion/react-native-gesture-handler/) 70 | 71 | You first need to add them as dependencies: 72 | 73 | ```bash 74 | npm install react-native-reanimated react-native-gesture-handler --save 75 | # or 76 | yarn add react-native-reanimated react-native-gesture-handler 77 | # or 78 | pnpm add react-native-reanimated react-native-gesture-handler 79 | ``` 80 | 81 | Then you must follow their respective installation instructions: 82 | 83 | - [react-native-reanimated](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation) 84 | - [react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/docs/installation) 85 | 86 | ## Quickstart 87 | 88 | ```tsx 89 | import { DndProvider, DndProviderProps, Draggable, Droppable } from "@mgcrea/react-native-dnd"; 90 | import type { FunctionComponent } from "react"; 91 | import { SafeAreaView, StyleSheet, Text } from "react-native"; 92 | import { GestureHandlerRootView, State } from "react-native-gesture-handler"; 93 | 94 | export const App: FunctionComponent = () => { 95 | const handleDragEnd: DndProviderProps["onDragEnd"] = ({ active, over }) => { 96 | "worklet"; 97 | if (over) { 98 | console.log("onDragEnd", { active, over }); 99 | } 100 | }; 101 | 102 | const handleBegin: DndProviderProps["onBegin"] = () => { 103 | "worklet"; 104 | console.log("onBegin"); 105 | }; 106 | 107 | const handleFinalize: DndProviderProps["onFinalize"] = ({ state }) => { 108 | "worklet"; 109 | console.log("onFinalize"); 110 | if (state !== State.FAILED) { 111 | console.log("onFinalize"); 112 | } 113 | }; 114 | 115 | return ( 116 | 117 | 118 | 119 | 120 | DROP 121 | 122 | 123 | DRAG 124 | 125 | 126 | 127 | 128 | ); 129 | }; 130 | 131 | const styles = StyleSheet.create({ 132 | box: { 133 | margin: 24, 134 | padding: 24, 135 | height: 128, 136 | width: 128, 137 | justifyContent: "center", 138 | alignItems: "center", 139 | backgroundColor: "darkseagreen", 140 | }, 141 | }); 142 | ``` 143 | 144 | ## Credits 145 | 146 | Inspired by: 147 | 148 | - [clauderic/dnd-kit](https://github.com/clauderic/dnd-kit) 149 | 150 | ## Authors 151 | 152 | - [Olivier Louvignes](https://github.com/mgcrea) <> 153 | 154 | ## License 155 | 156 | ```txt 157 | The MIT License 158 | 159 | Copyright (c) 2023 Olivier Louvignes 160 | 161 | Permission is hereby granted, free of charge, to any person obtaining a copy 162 | of this software and associated documentation files (the "Software"), to deal 163 | in the Software without restriction, including without limitation the rights 164 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 165 | copies of the Software, and to permit persons to whom the Software is 166 | furnished to do so, subject to the following conditions: 167 | 168 | The above copyright notice and this permission notice shall be included in 169 | all copies or substantial portions of the Software. 170 | 171 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 172 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 173 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 174 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 175 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 176 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 177 | THE SOFTWARE. 178 | ``` 179 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('@babel/core').TransformOptions} */ 2 | module.exports = { 3 | presets: ["module:@react-native/babel-preset"], 4 | plugins: ["react-native-reanimated/plugin"], 5 | }; 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /docs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 111, 3 | "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss", "prettier-plugin-astro"], 4 | "overrides": [ 5 | { 6 | "files": "*.astro", 7 | "options": { 8 | "parser": "astro" 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Astro Starter Kit: Basics 2 | 3 | ```sh 4 | npm create astro@latest -- --template basics 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) 9 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) 10 | 11 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 | 13 | ![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) 14 | 15 | ## 🚀 Project Structure 16 | 17 | Inside of your Astro project, you'll see the following folders and files: 18 | 19 | ```text 20 | / 21 | ├── public/ 22 | │ └── favicon.svg 23 | ├── src/ 24 | │ ├── components/ 25 | │ │ └── Card.astro 26 | │ ├── layouts/ 27 | │ │ └── Layout.astro 28 | │ └── pages/ 29 | │ └── index.astro 30 | └── package.json 31 | ``` 32 | 33 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 34 | 35 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 36 | 37 | Any static assets, like images, can be placed in the `public/` directory. 38 | 39 | ## 🧞 Commands 40 | 41 | All commands are run from the root of the project, from a terminal: 42 | 43 | | Command | Action | 44 | | :------------------------ | :----------------------------------------------- | 45 | | `npm install` | Installs dependencies | 46 | | `npm run dev` | Starts local dev server at `localhost:4321` | 47 | | `npm run build` | Build your production site to `./dist/` | 48 | | `npm run preview` | Preview your build locally, before deploying | 49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 50 | | `npm run astro -- --help` | Get help using the Astro CLI | 51 | 52 | ## 👀 Want to learn more? 53 | 54 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 55 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import mdx from "@astrojs/mdx"; 2 | import react from "@astrojs/react"; 3 | import tailwind from "@astrojs/tailwind"; 4 | import { defineConfig } from "astro/config"; 5 | import preact from "@astrojs/preact"; 6 | import rehypeExternalLinks from "rehype-external-links"; 7 | import MDXCodeBlocks from "astro-mdx-code-blocks"; 8 | 9 | // https://astro.build/config 10 | export default defineConfig({ 11 | markdown: { 12 | rehypePlugins: [ 13 | [ 14 | rehypeExternalLinks, 15 | { 16 | content: { 17 | type: "text", 18 | value: " 🔗", 19 | }, 20 | }, 21 | ], 22 | ], 23 | syntaxHighlight: "shiki", 24 | shikiConfig: { 25 | theme: "one-dark-pro", 26 | }, 27 | }, 28 | integrations: [ 29 | tailwind({ 30 | applyBaseStyles: false, 31 | }), 32 | mdx(), 33 | react(), 34 | preact(), 35 | MDXCodeBlocks(), 36 | ], 37 | site: `https://mgcrea.github.io`, 38 | base: "/react-native-dnd", 39 | }); 40 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "prettify": "prettier --write src", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@astrojs/check": "^0.3.4", 15 | "@astrojs/mdx": "^2.0.2", 16 | "@astrojs/preact": "^3.0.1", 17 | "@astrojs/react": "^3.0.8", 18 | "@astrojs/tailwind": "^5.0.4", 19 | "@headlessui/react": "^1.7.17", 20 | "@types/react": "^18.2.45", 21 | "@types/react-dom": "^18.2.18", 22 | "astro": "^4.0.7", 23 | "preact": "^10.19.3", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "tailwindcss": "^3.4.0", 27 | "typescript": "^5.3.3", 28 | "zustand": "^4.4.7" 29 | }, 30 | "devDependencies": { 31 | "@tailwindcss/typography": "^0.5.10", 32 | "astro-mdx-code-blocks": "^0.0.6", 33 | "clsx": "^2.0.0", 34 | "prettier-plugin-astro": "^0.12.2", 35 | "prettier-plugin-organize-imports": "^3.2.4", 36 | "prettier-plugin-tailwindcss": "^0.5.9", 37 | "rehype-external-links": "^3.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /docs/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { AnchorHTMLAttributes, FunctionComponent, PropsWithChildren } from "react"; 3 | import { BASE_URL } from "src/config/env"; 4 | 5 | export type LinkProps = AnchorHTMLAttributes; 6 | 7 | export const Link: FunctionComponent> = ({ 8 | children, 9 | href: hrefProp, 10 | ...otherProps 11 | }) => { 12 | const href = hrefProp?.startsWith("/") ? `${BASE_URL}${hrefProp}` : hrefProp; 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /docs/src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent, HTMLAttributes, SVGAttributes } from "react"; 3 | 4 | export type LogoIconProps = SVGAttributes; 5 | 6 | export const LogoIcon: FunctionComponent = (props) => { 7 | return ( 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 23 | {/* 24 | 25 | */} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export type LogoProps = HTMLAttributes; 37 | 38 | export const Logo: FunctionComponent = (props) => { 39 | return ( 40 |
41 | 42 |
43 | React Native 44 | 45 | Drag 46 | & 47 | Drop 48 | 49 |
50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /docs/src/components/Search.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | // import { DocSearchModal, useDocSearchKeyboardEvents } from "@docsearch/react"; 3 | // import Link from "next/link"; 4 | // import Router from "next/router"; 5 | import { useCallback, useEffect, useState } from "react"; 6 | import { SearchIcon } from "./icon"; 7 | 8 | // const docSearchConfig = { 9 | // // appId: process.env.NEXT_PUBLIC_DOCSEARCH_APP_ID, 10 | // // apiKey: process.env.NEXT_PUBLIC_DOCSEARCH_API_KEY, 11 | // // indexName: process.env.NEXT_PUBLIC_DOCSEARCH_INDEX_NAME, 12 | // }; 13 | 14 | // function Hit({ hit, children }) { 15 | // return {children}; 16 | // } 17 | 18 | export function Search() { 19 | let [_isOpen, setIsOpen] = useState(false); 20 | let [modifierKey, setModifierKey] = useState(""); 21 | 22 | const onOpen = useCallback(() => { 23 | setIsOpen(true); 24 | }, [setIsOpen]); 25 | 26 | // const onClose = useCallback(() => { 27 | // setIsOpen(false); 28 | // }, [setIsOpen]); 29 | 30 | // useDocSearchKeyboardEvents({ isOpen, onOpen, onClose }); 31 | 32 | useEffect(() => { 33 | setModifierKey(/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? "⌘" : "Ctrl "); 34 | }, []); 35 | 36 | return ( 37 | <> 38 | 54 | {/* {isOpen && 55 | createPortal( 56 | , 67 | document.body 68 | )} */} 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /docs/src/components/icon/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent, SVGAttributes } from "react"; 3 | 4 | export type CloseIconProps = SVGAttributes; 5 | 6 | export const CloseIcon: FunctionComponent = (props) => { 7 | return ( 8 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /docs/src/components/icon/DarkIcon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent, SVGAttributes } from "react"; 3 | 4 | export type DarkIconProps = SVGAttributes; 5 | 6 | export const DarkIcon: FunctionComponent = (props) => { 7 | return ( 8 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /docs/src/components/icon/GitHubIcon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent, SVGAttributes } from "react"; 3 | 4 | export type GitHubIconProps = SVGAttributes; 5 | 6 | export const GitHubIcon: FunctionComponent = (props) => { 7 | return ( 8 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /docs/src/components/icon/LightIcon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent, SVGAttributes } from "react"; 3 | 4 | export type LightIconProps = SVGAttributes; 5 | 6 | export const LightIcon: FunctionComponent = (props) => { 7 | return ( 8 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /docs/src/components/icon/MenuIcon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent, SVGAttributes } from "react"; 3 | 4 | export type MenuIconProps = SVGAttributes; 5 | 6 | export const MenuIcon: FunctionComponent = (props) => { 7 | return ( 8 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /docs/src/components/icon/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent, SVGAttributes } from "react"; 3 | 4 | export type SearchIconProps = SVGAttributes; 5 | 6 | export const SearchIcon: FunctionComponent = (props) => { 7 | return ( 8 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /docs/src/components/icon/SystemIcon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent, SVGAttributes } from "react"; 3 | 4 | export type SystemIconProps = SVGAttributes; 5 | 6 | export const SystemIcon: FunctionComponent = (props) => { 7 | return ( 8 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /docs/src/components/icon/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CloseIcon"; 2 | export * from "./DarkIcon"; 3 | export * from "./GitHubIcon"; 4 | export * from "./LightIcon"; 5 | export * from "./MenuIcon"; 6 | export * from "./SearchIcon"; 7 | export * from "./SystemIcon"; 8 | -------------------------------------------------------------------------------- /docs/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Link"; 2 | export * from "./layout"; 3 | -------------------------------------------------------------------------------- /docs/src/components/layout/Article.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent, HTMLAttributes, PropsWithChildren } from "react"; 3 | import { classNames } from "src/utils"; 4 | 5 | export type ArticleProps = HTMLAttributes; 6 | 7 | export const Article: FunctionComponent> = ({ 8 | children, 9 | className, 10 | ...otherProps 11 | }) => { 12 | return ( 13 |
32 | {children} 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /docs/src/components/layout/Header.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent } from "react"; 3 | import { GITHUB_REPO_URL } from "src/config/env"; 4 | import { useIsScrolled } from "src/hooks"; 5 | import { classNames } from "src/utils"; 6 | import { Link } from "../Link"; 7 | import { Logo } from "../Logo"; 8 | import { Search } from "../Search"; 9 | import { GitHubIcon } from "../icon"; 10 | import { MobileNavigation } from "./MobileNavigation"; 11 | 12 | export type HeaderProps = { currentPage: string }; 13 | 14 | export const Header: FunctionComponent = ({ currentPage }) => { 15 | const isScrolled = useIsScrolled(); 16 | return ( 17 |
25 |
26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 | 35 |
36 |
37 | {/* */} 38 | 39 | 40 | 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /docs/src/components/layout/MobileNavigation.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import { Popover, Transition } from "@headlessui/react"; 3 | import { Fragment, type FunctionComponent } from "react"; 4 | import { CloseIcon, MenuIcon } from "../icon"; 5 | import { Navigation } from "./Navigation"; 6 | 7 | type MenuToggleIconProps = { isOpen: boolean }; 8 | 9 | const MenuToggleIcon: FunctionComponent = ({ isOpen }) => { 10 | const ToggleIcon = isOpen ? CloseIcon : MenuIcon; 11 | return ( 12 | // 15 | ); 16 | }; 17 | 18 | export const MobileNavigation: FunctionComponent<{ currentPage: string }> = ({ currentPage }) => { 19 | // const [isOpen, setIsOpen] = useState(false); 20 | // const navigationStore = useNavigationStore()); 21 | // console.log({ navigation, navigationStore }); 22 | return ( 23 | 24 | {({ open }) => ( 25 | <> 26 | 27 | 28 | 29 | 38 | 39 | 40 | {/* {}} className="fixed inset-0 top-20 z-50 lg:hidden">*/} 41 | 50 | 51 |
52 | 56 |
57 |
58 |
59 | 60 | )} 61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /docs/src/components/layout/Navigation.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent, HTMLAttributes } from "react"; 3 | import { NAVIGATION_ITEMS } from "src/config/navigation"; 4 | import { classNames, ucfirst } from "src/utils"; 5 | import { Link } from "../Link"; 6 | 7 | export type NavigationProps = Pick, "className"> & { 8 | currentPage: string; 9 | }; 10 | 11 | export const Navigation: FunctionComponent = ({ className, currentPage }) => { 12 | // const items = useNavigationStore((state) => state.items); 13 | return ( 14 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /docs/src/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Article"; 2 | export * from "./Header"; 3 | export * from "./Navigation"; 4 | -------------------------------------------------------------------------------- /docs/src/components/markdown/CodeBlock.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Code } from "astro/components"; 3 | import { CopyButton } from "./components/CopyButton"; 4 | 5 | type CodeProps = Parameters[0]; 6 | 7 | interface Props { 8 | code: string; 9 | lang: CodeProps["lang"]; 10 | filename?: string; 11 | } 12 | const { code, lang, filename } = Astro.props; 13 | 14 | const hasLang = !!lang; 15 | const hasFileName = !!filename; 16 | const showHeader = hasLang || hasFileName; 17 | --- 18 | 19 |
20 | { 21 | showHeader && ( 22 |
23 | {hasFileName && // {filename}} 24 | {hasLang && ({lang})} 25 |
26 | ) 27 | } 28 |
29 | 30 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /docs/src/components/markdown/Properties.astro: -------------------------------------------------------------------------------- 1 |
2 |
    6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /docs/src/components/markdown/Property.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | name: string; 4 | type: string; 5 | optional?: boolean; 6 | defaultValue?: string; 7 | } 8 | const { name, type, optional, defaultValue } = Astro.props; 9 | --- 10 | 11 |
  • 12 |
    13 |
    Name
    14 |
    15 | {name} 16 |
    17 |
    Type
    18 |
    19 | :{type}{ 20 | optional ? ? : null 21 | } 22 |
    23 | { 24 | defaultValue ? ( 25 |
    26 | = 27 | {defaultValue} 28 |
    29 | ) : null 30 | } 31 |
    Description
    32 |
    33 | 34 |
    35 |
    36 |
  • 37 | -------------------------------------------------------------------------------- /docs/src/components/markdown/components/ClipboardIcon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource preact */ 2 | import type { FunctionComponent, JSX } from "preact"; 3 | 4 | export type ClipboardIconProps = JSX.SVGAttributes; 5 | 6 | export const ClipboardIcon: FunctionComponent = (props) => { 7 | return ( 8 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /docs/src/components/markdown/components/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource preact */ 2 | import type { FunctionalComponent as FunctionComponent } from "preact"; 3 | import { useEffect, useState } from "preact/hooks"; 4 | import { classNames } from "src/utils"; 5 | import { ClipboardIcon } from "./ClipboardIcon"; 6 | 7 | export type CopyButtonProps = { 8 | code: string; 9 | }; 10 | 11 | export const CopyButton: FunctionComponent = ({ code }) => { 12 | let [copyCount, setCopyCount] = useState(0); 13 | let wasCopied = copyCount > 0; 14 | 15 | useEffect(() => { 16 | if (copyCount === 0) { 17 | return; 18 | } 19 | let timeout = setTimeout(() => setCopyCount(0), 1000); 20 | return () => { 21 | clearTimeout(timeout); 22 | }; 23 | }, [copyCount]); 24 | 25 | const handleClick = async () => { 26 | await window.navigator.clipboard.writeText(code); 27 | setCopyCount((count) => count + 1); 28 | }; 29 | 30 | return ( 31 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /docs/src/components/markdown/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | export { default as CodeBlock } from "./CodeBlock.astro"; 3 | // @ts-ignore 4 | export { default as Properties } from "./Properties.astro"; 5 | // @ts-ignore 6 | export { default as Property } from "./Property.astro"; 7 | -------------------------------------------------------------------------------- /docs/src/config/env.ts: -------------------------------------------------------------------------------- 1 | const { BASE_URL: ASTRO_BASE_URL } = import.meta.env; 2 | 3 | export const BASE_URL = ASTRO_BASE_URL.replace(/"/g, ""); 4 | 5 | export const SITE = { 6 | title: "React Native DND", 7 | description: "Your website description.", 8 | defaultLanguage: "en-us", 9 | } as const; 10 | 11 | // export const OPEN_GRAPH = { 12 | // image: { 13 | // src: "https://github.com/withastro/astro/blob/main/.github/assets/banner.png?raw=true", 14 | // alt: 15 | // "astro logo on a starry expanse of space," + 16 | // " with a purple saturn-like planet floating in the right foreground", 17 | // }, 18 | // twitter: "astrodotbuild", 19 | // }; 20 | 21 | export const KNOWN_LANGUAGES = { 22 | English: "en", 23 | } as const; 24 | export const KNOWN_LANGUAGE_CODES = Object.values(KNOWN_LANGUAGES); 25 | 26 | export const GITHUB_REPO_URL = `https://github.com/mgcrea/react-native-dnd`; 27 | export const GITHUB_EDIT_URL = `https://github.com/withastro/astro/tree/main/examples/docs`; 28 | 29 | // See "Algolia" section of the README for more information. 30 | // export const ALGOLIA = { 31 | // indexName: "XXXXXXXXXX", 32 | // appId: "XXXXXXXXXX", 33 | // apiKey: "XXXXXXXXXX", 34 | // }; 35 | 36 | // export type Sidebar = Record< 37 | // (typeof KNOWN_LANGUAGE_CODES)[number], 38 | // Record 39 | // >; 40 | // export const SIDEBAR: Sidebar = { 41 | // en: { 42 | // "Section Header": [ 43 | // { text: "Introduction", link: "en/introduction" }, 44 | // { text: "Page 2", link: "en/page-2" }, 45 | // { text: "Page 3", link: "en/page-3" }, 46 | // { text: "Quickstart", link: "en/quickstart" }, 47 | // { text: "Quickstart 2", link: "en/quickstart2" }, 48 | // ], 49 | // "Another Section": [{ text: "Page 4", link: "en/page-4" }], 50 | // }, 51 | // }; 52 | -------------------------------------------------------------------------------- /docs/src/config/navigation.ts: -------------------------------------------------------------------------------- 1 | import { getCollection } from "astro:content"; 2 | 3 | const docs = await getCollection("docs"); 4 | 5 | const SECTIONS_ORDER = ["guides", "providers", "components", "hooks", "utils"]; 6 | 7 | export type NavigationItem = { 8 | title: string; 9 | order: number; 10 | href: string; 11 | }; 12 | 13 | const docsBySection = docs.reduce>((soFar, entry) => { 14 | const path = entry.slug.split("/"); 15 | const { title, order } = entry.data; 16 | const navigationItem: NavigationItem = { title, order, href: `/${entry.slug}` }; 17 | const sectionSlug = String(path[0]); 18 | if (!soFar[sectionSlug]) { 19 | soFar[sectionSlug] = []; 20 | } 21 | soFar[sectionSlug]?.push(navigationItem); 22 | return soFar; 23 | }, {}); 24 | 25 | export const NAVIGATION_ITEMS = Object.entries(docsBySection).sort(([a], [b]) => 26 | SECTIONS_ORDER.indexOf(a) < SECTIONS_ORDER.indexOf(b) ? -1 : 1, 27 | ); 28 | 29 | NAVIGATION_ITEMS.forEach(([_, items]) => { 30 | items.sort((a, b) => a.order - b.order); 31 | }); 32 | 33 | // console.dir(NAVIGATION_ITEMS, { depth: null }); 34 | -------------------------------------------------------------------------------- /docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection, z } from "astro:content"; 2 | import { SITE } from "src/config/env"; 3 | 4 | const docs = defineCollection({ 5 | schema: z.object({ 6 | title: z.string().default(SITE.title), 7 | description: z.string().default(SITE.description), 8 | order: z.number().default(99), 9 | lang: z.literal("en-us").default(SITE.defaultLanguage), 10 | dir: z.union([z.literal("ltr"), z.literal("rtl")]).default("ltr"), 11 | image: z 12 | .object({ 13 | src: z.string(), 14 | alt: z.string(), 15 | }) 16 | .optional(), 17 | ogLocale: z.string().optional(), 18 | }), 19 | }); 20 | 21 | export const collections = { docs }; 22 | -------------------------------------------------------------------------------- /docs/src/content/docs/components/Draggable.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Draggable" 3 | --- 4 | 5 | # {frontmatter.title} 6 | 7 | `Draggable` is a reusable React component that can be used to quickly make elements draggable within a Drag and Drop context. 8 | 9 | For most use cases, you should use the source of this component as a blueprint to create a custom draggable component using [useDraggable](./../hooks/usedraggable). 10 | 11 | ## Usage 12 | 13 | ```tsx 14 | // src/components/SomeComponent.tsx 15 | import { Draggable } from "@mgcrea/react-native-dnd"; 16 | 17 | const SomeComponent = () => { 18 | const id = "unique-id"; 19 | const data = { key: "value" }; 20 | 21 | return ( 22 | 23 | {/* Render draggable content */} 24 | 25 | ); 26 | }; 27 | ``` 28 | 29 | ## Props 30 | 31 | The `Draggable` component accepts the following props: 32 | 33 | --- 34 | 35 | 36 | 37 | A unique identifier for the draggable component. 38 | 39 | 40 | An optional data object that can be used to store additional information about the draggable component. 41 | Default is an empty object (`{}`). 42 | 43 | 44 | A boolean indicating whether the draggable component should be disabled (i.e., not be able to be dragged). 45 | Default is `false`. 46 | 47 | 48 | An optional style object to apply custom styles to the draggable component. 49 | 50 | 51 | An optional animated style worklet that returns the animated style for the draggable component. The worklet 52 | function takes in the style object and a boolean `isActive` which is `true` if the component is active and 53 | `false` otherwise. 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/src/content/docs/components/DraggableGrid.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "DraggableGrid" 3 | --- 4 | 5 | # {frontmatter.title} 6 | 7 | `DraggableGrid` enables you to easily create a grid of sortable draggable components. 8 | 9 | It is built on top of the [useDragableGrid](./../hooks/usedraggablegrid) hook. 10 | 11 | This component does not handle variable sized items for now (all items must have the same size). 12 | 13 | ## Usage 14 | 15 | ```tsx 16 | import { 17 | DndProvider, 18 | type ObjectWithId, 19 | Draggable, 20 | DraggableGrid, 21 | DraggableGridProps, 22 | } from "@mgcrea/react-native-dnd/src"; 23 | import { type FunctionComponent } from "react"; 24 | import { StyleSheet, Text, View } from "react-native"; 25 | 26 | const GRID_SIZE = 3; 27 | const items: string[] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; 28 | const data = items.map((letter, index) => ({ 29 | id: `${index}-${letter}`, 30 | value: letter, 31 | })) satisfies ObjectWithId[]; 32 | 33 | export const DraggableGridExample: FunctionComponent = () => { 34 | const onGridOrderChange: DraggableGridProps["onOrderChange"] = (value) => { 35 | console.log("onGridOrderChange", value); 36 | }; 37 | 38 | return ( 39 | 40 | DraggableGrid{"\n"}Example 41 | 42 | 43 | {data.map((item) => ( 44 | 45 | {item.value} 46 | 47 | ))} 48 | 49 | 50 | 51 | ); 52 | }; 53 | ``` 54 | 55 | ## Props 56 | 57 | The `DraggableGrid` component accepts the following props: 58 | 59 | --- 60 | 61 | 62 | 63 | The direction of the grid. Can be either `row` or `column`, `row-reverse` or `column-reverse`. Default is 64 | `row`. 65 | 66 | 67 | The size of the grid accross it's main axis. For example, if the direction is `row`, this is the number of 68 | columns in the grid. If the direction is `column`, this is the number of rows in the grid. 69 | 70 | 71 | The gap between items in the grid. Default is `0`. 72 | 73 | 74 | An optional style object to apply custom styles to the container View. 75 | 76 | 77 | An optional function that is called when the order of the items in the grid changes after a drag & sort 78 | operation. The function receives an array of the item ids in the new order. 79 | 80 | 85 | An optional function that is called every time the order of the items in the grid changes during a drag & 86 | sort operation. The function receives an array of the item ids in the new order, and an array of the item 87 | ids in the previous order. 88 | 89 | 90 | An optional function that can be used to override the default behavior of when to swap items. The function 91 | receives the following arguments: `(activeLayout: Rectangle, itemLayout: Rectangle) => boolean`. The 92 | function should return `true` if the items should be swapped, or `false` if they should not be swapped. 93 | 94 | 95 | -------------------------------------------------------------------------------- /docs/src/content/docs/components/DraggableStack.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "DraggableStack" 3 | --- 4 | 5 | # {frontmatter.title} 6 | 7 | `DraggableStack` enables you to easily create a stack of sortable draggable components. 8 | 9 | It is built on top of the [useDragableStack](./../hooks/usedraggablestack) hook. 10 | 11 | This component does support variable width and height items. 12 | 13 | ## Demo 14 | 15 | ![demo](https://raw.githubusercontent.com/mgcrea/react-native-dnd/master/.github/assets/DraggableStackExample.gif) 16 | 17 | ## Usage 18 | 19 | ```tsx 20 | import React, { type FunctionComponent } from "react"; 21 | import { StyleSheet, Text, View } from "react-native"; 22 | import { 23 | DndProvider, 24 | type ObjectWithId, 25 | Draggable, 26 | DraggableStack, 27 | type DraggableStackProps, 28 | } from "@mgcrea/react-native-dnd/src"; 29 | 30 | const items = ["🤓", "🤖🤖", "👻👻👻", "👾👾👾👾"]; 31 | const data = items.map((letter, index) => ({ 32 | value: letter, 33 | id: `${index}-${letter}`, 34 | })) satisfies ObjectWithId[]; 35 | 36 | export const DraggableStackExample: FunctionComponent = () => { 37 | const onStackOrderChange: DraggableStackProps["onOrderChange"] = (value) => { 38 | console.log("onStackOrderChange", value); 39 | }; 40 | const onStackOrderUpdate: DraggableStackProps["onOrderUpdate"] = (value) => { 41 | console.log("onStackOrderUpdate", value); 42 | }; 43 | 44 | return ( 45 | 46 | DraggableStack Example 47 | 48 | 55 | {data.map((letter) => ( 56 | 57 | {letter.value} 58 | 59 | ))} 60 | 61 | 62 | 63 | ); 64 | }; 65 | ``` 66 | 67 | ## Props 68 | 69 | The `DraggableStack` component accepts the following props: 70 | 71 | --- 72 | 73 | 74 | 75 | The direction of the grid. Can be either `row` or `column`, `row-reverse` or `column-reverse`. Default is 76 | `row`. 77 | 78 | 79 | The gap between items in the grid. Default is `0`. 80 | 81 | 82 | An optional style object to apply custom styles to the container View. 83 | 84 | 85 | An optional function that is called when the order of the items in the grid changes after a drag & sort 86 | operation. The function receives an array of the item ids in the new order. 87 | 88 | 93 | An optional function that is called every time the order of the items in the grid changes during a drag & 94 | sort operation. The function receives an array of the item ids in the new order, and an array of the item 95 | ids in the previous order. 96 | 97 | 98 | An optional function that can be used to override the default behavior of when to swap items. The function 99 | receives the following arguments: `(activeLayout: Rectangle, itemLayout: Rectangle) => boolean`. The 100 | function should return `true` if the items should be swapped, or `false` if they should not be swapped. 101 | 102 | 103 | -------------------------------------------------------------------------------- /docs/src/content/docs/components/Droppable.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Droppable" 3 | --- 4 | 5 | # {frontmatter.title} 6 | 7 | `Droppable` is a reusable React component that can be used to quickly make elements droppable within a Drag and Drop context. 8 | 9 | For most use cases, you should use the source of this component as a blueprint to create your custom droppable component using [useDroppable](./../hooks/usedroppable). 10 | 11 | ## Usage 12 | 13 | ```tsx 14 | // src/components/SomeComponent.tsx 15 | import { Droppable } from "@mgcrea/react-native-dnd"; 16 | 17 | const SomeComponent = () => { 18 | const id = "unique-id"; 19 | const data = { key: "value" }; 20 | 21 | return ( 22 | 23 | {/* Render droppable content */} 24 | 25 | ); 26 | }; 27 | ``` 28 | 29 | ## Props 30 | 31 | The `Droppable` component accepts the following props: 32 | 33 | --- 34 | 35 | 36 | 37 | A unique identifier for the droppable component. 38 | 39 | 40 | An optional data object that can be used to store additional information about the droppable component. 41 | Default is an empty object (`{}`). 42 | 43 | 44 | A boolean indicating whether the droppable component should be disabled (i.e., not accept any draggable 45 | components). Default is `false`. 46 | 47 | 48 | An optional style object to apply custom styles to the droppable component. 49 | 50 | 51 | An optional animated style worklet that returns the animated style for the droppable component. The worklet 52 | function takes in the style object and a boolean `isActive` which is `true` if the component is active and 53 | `false` otherwise. 54 | 55 | 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/data.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Data payloads" 3 | description: "How can you pass data to/between draggable and droppable components?" 4 | order: 3 5 | --- 6 | 7 | # {frontmatter.title} 8 | 9 | Both `useDraggable` and `useDroppable` hooks accept a `data` prop that can be used to attach data to the draggable or droppable component. 10 | 11 | This data can be either a plain object or a reanimated shared value. If a plain object is passed, it will be automatically wrapped as a shared value. 12 | 13 | ### Directly using a shared value 14 | 15 | ```tsx 16 | // src/App.tsx 17 | import { DndProvider, DndProviderProps, Draggable, Droppable } from "@mgcrea/react-native-dnd"; 18 | import type { FunctionComponent } from "react"; 19 | import { SafeAreaView, StyleSheet, Text } from "react-native"; 20 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 21 | 22 | export const App: FunctionComponent = () => { 23 | const dynamicData = useSharedValue({ count: 0 }); 24 | 25 | const handleDragEnd: DndProviderProps["onDragEnd"] = ({ active, over }) => { 26 | "worklet"; 27 | if (over) { 28 | console.log(`Current count is ${active.data.value.count}`); 29 | dynamicData.value = { ...dynamicData.value, count: dynamicData.value.count + 1 }; 30 | } 31 | }; 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | DROP 39 | 40 | 41 | DRAG 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | const styles = StyleSheet.create({ 50 | box: { 51 | margin: 24, 52 | padding: 24, 53 | height: 128, 54 | width: 128, 55 | justifyContent: "center", 56 | alignItems: "center", 57 | backgroundColor: "darkseagreen", 58 | }, 59 | }); 60 | ``` 61 | 62 | ### Using a plain object 63 | 64 | ```tsx 65 | // src/App.tsx 66 | import { DndProvider, DndProviderProps, Draggable, Droppable } from "@mgcrea/react-native-dnd"; 67 | import type { FunctionComponent } from "react"; 68 | import { SafeAreaView, StyleSheet, Text } from "react-native"; 69 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 70 | 71 | export const App: FunctionComponent = () => { 72 | const [count, setCount] = useState(0); 73 | 74 | const onDragEnd = () => { 75 | setCount((count) => count + 1); 76 | }; 77 | 78 | const handleDragEnd: DndProviderProps["onDragEnd"] = ({ active, over }) => { 79 | "worklet"; 80 | if (over) { 81 | console.log(`Current count is ${active.data.value.count}`); 82 | runOnJS(onDragEnd)(); 83 | } 84 | }; 85 | 86 | return ( 87 | 88 | 89 | 90 | 91 | DROP 92 | 93 | 94 | DRAG 95 | 96 | 97 | 98 | 99 | ); 100 | }; 101 | 102 | const styles = StyleSheet.create({ 103 | box: { 104 | margin: 24, 105 | padding: 24, 106 | height: 128, 107 | width: 128, 108 | justifyContent: "center", 109 | alignItems: "center", 110 | backgroundColor: "darkseagreen", 111 | }, 112 | }); 113 | ``` 114 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/install.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Install" 3 | description: "Installation steps for react-native-dnd" 4 | order: 1 5 | --- 6 | 7 | # {frontmatter.title} 8 | 9 | ```bash 10 | npm install @mgcrea/react-native-dnd --save 11 | # or 12 | yarn add @mgcrea/react-native-dnd 13 | # or 14 | pnpm add @mgcrea/react-native-dnd 15 | ``` 16 | 17 | ### Dependencies 18 | 19 | This package relies on : 20 | 21 | - [react-native-reanimated@>=3](https://github.com/software-mansion/react-native-reanimated/) 22 | - [react-native-gesture-handler@>=2](https://github.com/software-mansion/react-native-gesture-handler/) 23 | 24 | You first need to add them as dependencies: 25 | 26 | ```bash 27 | npm install react-native-reanimated react-native-gesture-handler --save 28 | # or 29 | yarn add react-native-reanimated react-native-gesture-handler 30 | # or 31 | pnpm add react-native-reanimated react-native-gesture-handler 32 | ``` 33 | 34 | Then you must follow their respective installation instructions: 35 | 36 | - [react-native-reanimated](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation) 37 | - [react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/docs/installation) 38 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Introduction" 3 | description: "react-native-dnd documentation introduction" 4 | order: 0 5 | --- 6 | 7 | # {frontmatter.title} 8 | 9 | `react-native-dnd` is a modern and easy-to-use drag&drop library for react-native. 10 | 11 | - **Modern and future-proof:** Built on [react-native-reanimated v3](https://github.com/software-mansion/react-native-reanimated/) 12 | 13 | - **Hooks based:** Exposes powerful hooks [`useDraggable`](./../hooks/usedraggable) and [`useDroppable`](./../hooks/usedroppable) to build your own primitive components. 14 | 15 | - **Ready to use:** Out-of-the-box configurable components like [`Draggable`](./../components/draggable) to quickly get started. 16 | 17 | - **Powerful & Performant:** The implementation has been tailored for advanced use cases where performance is critical. It can be used for a wide range of use cases like sorting items. 18 | 19 | ## Demo 20 | 21 | ![demo](https://raw.githubusercontent.com/mgcrea/react-native-dnd/master/.github/assets/demo.gif) 22 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/quickstart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Quick Start" 3 | description: "Quick Start for react-native-dnd" 4 | order: 2 5 | --- 6 | 7 | # {frontmatter.title} 8 | 9 | You can find below a minimal example of a working app using `react-native-dnd`. 10 | 11 | Most of the time you will want to create custom [`Draggable`](./../components/draggable) and [`Droppable`](./../components/droppable) components that you can use in your app. 12 | 13 | ```tsx 14 | // src/App.tsx 15 | import { DndProvider, DndProviderProps, Draggable, Droppable } from "@mgcrea/react-native-dnd"; 16 | import type { FunctionComponent } from "react"; 17 | import { SafeAreaView, StyleSheet, Text } from "react-native"; 18 | import { GestureHandlerRootView, State } from "react-native-gesture-handler"; 19 | 20 | export const App: FunctionComponent = () => { 21 | const handleDragEnd: DndProviderProps["onDragEnd"] = ({ active, over }) => { 22 | "worklet"; 23 | if (over) { 24 | console.log("onDragEnd", { active, over }); 25 | } 26 | }; 27 | 28 | const handleBegin: DndProviderProps["onBegin"] = () => { 29 | "worklet"; 30 | console.log("onBegin"); 31 | }; 32 | 33 | const handleFinalize: DndProviderProps["onFinalize"] = ({ state }) => { 34 | "worklet"; 35 | console.log("onFinalize"); 36 | if (state !== State.FAILED) { 37 | console.log("onFinalize"); 38 | } 39 | }; 40 | 41 | return ( 42 | 43 | 44 | 45 | 46 | DROP 47 | 48 | 49 | DRAG 50 | 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | const styles = StyleSheet.create({ 58 | box: { 59 | margin: 24, 60 | padding: 24, 61 | height: 128, 62 | width: 128, 63 | justifyContent: "center", 64 | alignItems: "center", 65 | backgroundColor: "darkseagreen", 66 | }, 67 | }); 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/src/content/docs/hooks/useActiveDragReaction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useActiveDragReaction" 3 | --- 4 | 5 | # {frontmatter.title} 6 | 7 | `useActiveDragReaction` is a custom React Hook that listens to the changes in the active draggable component and triggers a callback when the specified component becomes active or inactive. 8 | 9 | ## Usage 10 | 11 | ```tsx 12 | // src/components/DraggableComponent.tsx 13 | import { useActiveDragReaction } from "@mgcrea/react-native-dnd"; 14 | import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"; 15 | 16 | const DraggableComponent = ({ id, style, children }) => { 17 | const scale = useSharedValue(1); 18 | 19 | useActiveDragReaction(id, (isActive) => { 20 | "worklet"; 21 | console.log("Draggable component is active:", isActive); 22 | scale.value = withSpring(isActive ? 1.1 : 1); 23 | }); 24 | 25 | const animatedStyle = useAnimatedStyle(() => { 26 | return { 27 | transform: [ 28 | { 29 | scale: scale.value, 30 | }, 31 | ], 32 | }; 33 | }, [id]); 34 | 35 | // Render component content 36 | return {children}; 37 | }; 38 | ``` 39 | 40 | ## Parameters 41 | 42 | `useActiveDropReaction` accepts the following parameters: 43 | 44 | 45 | 46 | A unique identifier for the draggable component. 47 | 48 | 49 | A callback function that is called when the specified droppable component's active state changes. The 50 | function receives a boolean `isActive` which is `true` if the component is active and `false` otherwise. 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/src/content/docs/hooks/useActiveDropReaction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useActiveDropReaction" 3 | --- 4 | 5 | # {frontmatter.title} 6 | 7 | `useActiveDropReaction` is a custom React Hook that listens to the changes in the active droppable component and triggers a callback when the specified component becomes active or inactive. 8 | 9 | ## Usage 10 | 11 | ```tsx 12 | // src/components/DroppableComponent.tsx 13 | import { useActiveDropReaction } from "@mgcrea/react-native-dnd"; 14 | import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"; 15 | 16 | const DroppableComponent = ({ id, style, children }) => { 17 | const rotateZ = useSharedValue(0); 18 | 19 | useActiveDropReaction(id, (isActive) => { 20 | "worklet"; 21 | console.log("Droppable component is active:", isActive); 22 | rotateZ.value = withSpring(isActive ? 10 : 0); 23 | }); 24 | 25 | const animatedStyle = useAnimatedStyle(() => { 26 | return { 27 | transform: [ 28 | { 29 | rotateZ: `${rotateZ.value}deg`, 30 | }, 31 | ], 32 | }; 33 | }, [id]); 34 | 35 | // Render component content 36 | return {children}; 37 | }; 38 | ``` 39 | 40 | ## Parameters 41 | 42 | `useActiveDropReaction` accepts the following parameters: 43 | 44 | 45 | 46 | A unique identifier for the droppable component. 47 | 48 | 49 | A callback function that is called when the specified droppable component's active state changes. The 50 | function receives a boolean `isActive` which is `true` if the component is active and `false` otherwise. 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/src/content/docs/hooks/useDraggable.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useDraggable" 3 | order: 0 4 | --- 5 | 6 | # {frontmatter.title} 7 | 8 | `useDraggable` is a custom React Hook that enables a component to become draggable for drag-and-drop operations. This hook manages the registration, layout, and state of a draggable component. 9 | 10 | ## Minimal Example 11 | 12 | ```tsx 13 | // src/components/DraggableComponent.tsx 14 | import { useDraggable } from "@mgcrea/react-native-dnd"; 15 | 16 | const DraggableComponent = ({ id, data, disabled }) => { 17 | const { offset, setNodeRef, activeId, setNodeLayout, draggableState } = useDraggable({ 18 | id, 19 | data, 20 | disabled, 21 | }); 22 | 23 | return ( 24 | 25 | {/* Render component content */} 26 | 27 | ); 28 | }; 29 | ``` 30 | 31 | ## Advanced Usage 32 | 33 | ```tsx 34 | // src/components/MyDraggable.tsx 35 | import { useDraggable, type AnimateProps, type UseDraggableOptions } from "@mgcrea/react-native-dnd"; 36 | import { type FunctionComponent, type PropsWithChildren } from "react"; 37 | import { type ViewProps } from "react-native"; 38 | import Animated, { useAnimatedStyle } from "react-native-reanimated"; 39 | 40 | export type MyDraggableProps = AnimateProps & UseDraggableOptions; 41 | 42 | export const MyDraggable: FunctionComponent> = ({ 43 | children, 44 | id, 45 | data, 46 | disabled, 47 | activationDelay, 48 | activationTolerance, 49 | style, 50 | ...otherProps 51 | }) => { 52 | const { setNodeRef, setNodeLayout, activeId, actingId, offset } = useDraggable({ 53 | id, 54 | data, 55 | disabled, 56 | activationDelay, 57 | activationTolerance, 58 | }); 59 | 60 | const animatedStyle = useAnimatedStyle(() => { 61 | const isActive = activeId.value === id; 62 | const isResting = actingId.value !== id; 63 | const style = { 64 | opacity: isActive ? 0.9 : 1, 65 | zIndex: isActive ? 999 : 1, 66 | transform: [ 67 | { 68 | translateX: offset.x.value, 69 | }, 70 | { 71 | translateY: offset.y.value, 72 | }, 73 | ], 74 | }; 75 | return style; 76 | }, [id]); 77 | 78 | return ( 79 | 80 | {children} 81 | 82 | ); 83 | }; 84 | ``` 85 | 86 | ## Parameters 87 | 88 | `useDraggable` accepts an object with the following properties: 89 | 90 | 91 | 92 | A unique identifier for the draggable component. 93 | 94 | 95 | An optional data object that can be used to store additional information about the draggable component. 96 | Default is an empty object (`{}`). 97 | 98 | 99 | A number representing the duration, in milliseconds, that this draggable item needs to be held for before 100 | allowing a drag to start. Default is `0`. 101 | 102 | 103 | A number representing the distance, in points, of motion that is tolerated before the drag operation is 104 | aborted. 105 | 106 | 107 | A boolean indicating whether the draggable component should be disabled (i.e., not be able to be dragged). 108 | Default is `false`. 109 | 110 | 111 | 112 | ## Return values 113 | 114 | `useDraggable` returns an object with the following properties: 115 | 116 | 117 | 118 | A `SharedPoint` containing the x and y offsets of the draggable component. 119 | 120 | `}> 121 | A ref callback function used to store a reference to the draggable component's `Animated.View`. 122 | 123 | 124 | An `onLayout` event handler that should be attached to the draggable component's `Animated.View` to track 125 | its layout. 126 | 127 | `}> 128 | A `SharedValue` containing the ID of the currently active draggable component (eg. being interacted with) 129 | or `null` if there's no active draggable component. 130 | 131 | `}> 132 | A `SharedValue` containing the ID of the currently acting draggable component (eg. not at rest) or `null` 133 | if there's no acting draggable component. 134 | 135 | `}> 136 | A `SharedValue` containing the state of the currently dragged component. 137 | 138 | 139 | ``` 140 | -------------------------------------------------------------------------------- /docs/src/content/docs/hooks/useDraggableActiveId.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useDraggableActiveId" 3 | --- 4 | 5 | # {frontmatter.title} 6 | 7 | `useDraggableActiveId` is a custom React Hook that listens for changes in the active draggable component and returns the ID of the currently active draggable component or `null` if there is no active draggable component. 8 | 9 | ## Usage 10 | 11 | ```tsx 12 | // src/components/SomeComponent.tsx 13 | import { useDraggableActiveId } from "@mgcrea/react-native-dnd"; 14 | 15 | const SomeComponent = ({ id, style, children }) => { 16 | const activeId = useDraggableActiveId(); 17 | 18 | // Render component content 19 | }; 20 | ``` 21 | 22 | ## Interface 23 | 24 | `useDraggableActiveId` returns a state value containing the ID of the currently active draggable component (`UniqueIdentifier`) or `null` if there is no active draggable component. 25 | -------------------------------------------------------------------------------- /docs/src/content/docs/hooks/useDroppable.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useDroppable" 3 | description: "Lorem ipsum dolor sit amet - 4" 4 | order: 1 5 | --- 6 | 7 | # {frontmatter.title} 8 | 9 | `useDroppable` is a custom React Hook that allows a component to become a droppable target for drag-and-drop operations. This hook manages the registration, layout, and state of a droppable component. 10 | 11 | ## Minimal Example 12 | 13 | ```tsx 14 | // src/components/DroppableComponent.tsx 15 | import { useDroppable } from "@mgcrea/react-native-dnd"; 16 | 17 | const DroppableComponent = ({ id, data, disabled }) => { 18 | const { setNodeRef, setNodeLayout, activeId, draggableState } = useDroppable({ 19 | id, 20 | data, 21 | disabled, 22 | }); 23 | 24 | return ( 25 | 26 | {/* Render component content */} 27 | 28 | ); 29 | }; 30 | ``` 31 | 32 | ## Advanced Usage 33 | 34 | ```tsx 35 | // src/components/MyDroppable.tsx 36 | import { type AnimateProps } from "@mgcrea/react-native-dnd"; 37 | import { type FunctionComponent, type PropsWithChildren } from "react"; 38 | import { type ViewProps } from "react-native"; 39 | import Animated, { useAnimatedStyle } from "react-native-reanimated"; 40 | import { type UseDraggableOptions } from "../hooks"; 41 | 42 | export type MyDroppableProps = AnimateProps & UseDraggableOptions; 43 | 44 | export const MyDroppable: FunctionComponent> = ({ 45 | children, 46 | id, 47 | disabled, 48 | data, 49 | style, 50 | ...otherProps 51 | }) => { 52 | const { setNodeRef, setNodeLayout, activeId } = useDroppable({ 53 | id, 54 | disabled, 55 | data, 56 | }); 57 | 58 | const animatedStyle = useAnimatedStyle(() => { 59 | const isActive = activeId.value === id; 60 | const style = { 61 | opacity: isActive ? 0.9 : 1, 62 | }; 63 | if (animatedStyleWorklet) { 64 | animatedStyleWorklet(style, isActive); 65 | } 66 | return style; 67 | }, [id]); 68 | 69 | return ( 70 | 71 | {children} 72 | 73 | ); 74 | }; 75 | ``` 76 | 77 | ### Parameters 78 | 79 | `useDroppable` accepts an object with the following properties: 80 | 81 | 82 | 83 | A unique identifier for the droppable component. 84 | 85 | 86 | An optional data object that can be used to store additional information about the droppable component. 87 | Default is an empty object (`{}`). 88 | 89 | 90 | A boolean indicating whether the droppable component should be disabled (i.e., not accept any draggable 91 | components). Default is `false`. 92 | 93 | 94 | 95 | ### Interface 96 | 97 | `useDroppable` returns an object with the following properties: 98 | 99 | 100 | `}> 101 | A ref callback function used to store a reference to the droppable component's `Animated.View`. 102 | 103 | 104 | An `onLayout` event handler that should be attached to the droppable component's `Animated.View` to track 105 | its layout. 106 | 107 | `}> 108 | A `SharedValue` containing the ID of the currently active droppable component or `null` if there's no 109 | active droppable component. 110 | 111 | `}> 112 | A `SharedValue` containing the state of the currently dragged component. 113 | 114 | 115 | ``` 116 | -------------------------------------------------------------------------------- /docs/src/content/docs/providers/DndProvider.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "DndProvider" 3 | --- 4 | 5 | # {frontmatter.title} 6 | 7 | `DndProvider` is a React component that sets up the Drag and Drop context (`DndContext`) for its children components. 8 | 9 | It provides the necessary state and logic for draggable and droppable components. 10 | 11 | ## Usage 12 | 13 | ```tsx 14 | // src/App.tsx 15 | import { DndProvider } from "path/to/DndProvider"; 16 | import type { FunctionComponent } from "react"; 17 | 18 | export const App: FunctionComponent = () => { 19 | return {/* Render draggable and droppable components */}; 20 | }; 21 | ``` 22 | 23 | ## Props 24 | 25 | `DndProvider` component accepts the following props: 26 | 27 | 28 | 29 | An optional spring configuration object to be used for animating the dragged item back to its initial 30 | position. Default is `{}`. 31 | 32 | 33 | An optional number representing the duration, in milliseconds, of the LongPress gesture before Pan is 34 | allowed to activate. If the finger is moved during that period, the gesture will fail. Default is `0`. 35 | 36 | 37 | An optional number representing minimum distance the finger (or multiple finger) need to travel before the 38 | gesture activates. Expressed in points. 39 | 40 | 41 | An optional boolean value to disable the Drag and Drop functionality globally. Default is `false`. 42 | 43 | 44 | A callback function that is called when a draggable item becomes active. Can be used to trigger haptic 45 | feedback. 46 | 47 | 48 | A callback function to be called when a drag event ends. It receives an object with `active` and `over` 49 | properties representing the active draggable item options and the droppable item options that the draggable 50 | item is over, respectively. 51 | 52 | 53 | A callback function to be called when a drag event begins. It receives the gesture event and an object with 54 | `activeId` and `activeLayout` properties representing the active draggable item's ID and layout, 55 | respectively. 56 | 57 | 58 | A callback function to be called when a drag event updates. It receives the gesture event and an object 59 | with `activeId` and `activeLayout` properties representing the active draggable item's ID and updated 60 | layout, respectively. 61 | 62 | 63 | A callback function to be called when a drag event is finalized. It receives the gesture event and an 64 | object with `activeId` and `activeLayout` properties representing the active draggable item's ID and 65 | updated layout, respectively. 66 | 67 | 68 | A worklet callback function to be called on gesture update to determine if the active draggable item can be 69 | dropped on a droppable item. It receives the active Rectangle layout and a potentially droppable Rectangle 70 | layout 71 | 72 | 73 | An optional style object to apply custom styles to the DndProvider component. 74 | 75 | 76 | The draggable and droppable components to be rendered within the Drag and Drop context. 77 | 78 | 79 | -------------------------------------------------------------------------------- /docs/src/content/docs/utils/useEvent.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useEvent" 3 | --- 4 | 5 | `useEvent` is a custom React Hook to define an event handler with a function identity that is always stable. 6 | 7 | The event handler will be referentially the same during each re-render. Essentially, the event handler will have the following properties: 8 | 9 | - The function will not be recreated each time prop or state changes 10 | 11 | - The function will have access to the latest value of both the prop and state 12 | 13 | See this [LogRocket blog post](https://blog.logrocket.com/what-you-need-know-react-useevent-hook-rfc) for more information 14 | 15 | ## Usage 16 | 17 | ```tsx 18 | const SomeComponent = ({ someProp }) => { 19 | const [someState, setSomeState] = useState(0); 20 | 21 | const handleEvent = useEvent(() => { 22 | console.log(`log some state: `, someState); 23 | }); 24 | 25 | // Render component content 26 | }; 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/src/content/docs/utils/useLatestValue.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useLatestValue" 3 | --- 4 | 5 | `useLatestValue` is a custom React Hook that keeps the latest value of a variable inside a `ref` object, which is not subject to component re-renders. This is particularly useful when you need to access the latest value of a prop or state variable within a callback or a nested function. 6 | 7 | ## Usage 8 | 9 | ```tsx 10 | import { useLatestValue } from "path/to/useLatestValue"; 11 | 12 | const SomeComponent = ({ someProp }) => { 13 | const latestValueRef = useLatestValue(someProp); 14 | 15 | // Render component content 16 | }; 17 | ``` 18 | 19 | ## Parameters 20 | 21 | `useLatestValue` accepts the following parameters: 22 | 23 | 24 | 25 | The value to be stored inside the ref object. 26 | 27 | 28 | An optional array of dependencies to update the value in the ref object when changed. Default is `[value]`. 29 | 30 | 31 | A boolean indicating whether the draggable component should be disabled (i.e., not be able to be dragged). 32 | Default is `false`. 33 | 34 | 35 | 36 | ## Return values 37 | 38 | `useLatestValue` returns a `RefObject` containing the latest value of the input. 39 | -------------------------------------------------------------------------------- /docs/src/content/docs/utils/useNodeRef.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useNodeRef" 3 | --- 4 | 5 | `useNodeRef` is a custom React Hook to receive a stable ref setter with an optional onChange handler. 6 | 7 | ## Usage 8 | 9 | ```tsx 10 | const SomeComponent = ({ someProp }) => { 11 | const [node, setNodeRef] = useNodeRef(); 12 | 13 | return ; 14 | }; 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useDidMount"; 2 | export * from "./useIsScrolled"; 3 | export * from "./useNavigationStore"; 4 | -------------------------------------------------------------------------------- /docs/src/hooks/useDidMount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect, useState } from "react"; 2 | 3 | const useIsomorphicEffect = import.meta.env.SSR ? useEffect : useLayoutEffect; 4 | 5 | export const useDidMount = (): boolean => { 6 | const [didMount, setDidMount] = useState(false); 7 | 8 | useIsomorphicEffect(() => { 9 | setDidMount(true); 10 | }, []); 11 | 12 | return didMount; 13 | }; 14 | -------------------------------------------------------------------------------- /docs/src/hooks/useIsScrolled.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useIsScrolled = () => { 4 | let [isScrolled, setIsScrolled] = useState(false); 5 | 6 | useEffect(() => { 7 | function onScroll() { 8 | setIsScrolled(window.scrollY > 0); 9 | } 10 | onScroll(); 11 | window.addEventListener("scroll", onScroll, { passive: true }); 12 | return () => { 13 | window.removeEventListener("scroll", onScroll); 14 | }; 15 | }, []); 16 | 17 | return isScrolled; 18 | }; 19 | -------------------------------------------------------------------------------- /docs/src/hooks/useNavigationStore.ts: -------------------------------------------------------------------------------- 1 | import { NAVIGATION_ITEMS } from "src/config/navigation"; 2 | import { create } from "zustand"; 3 | 4 | type NavigationState = { 5 | isOpen: boolean; 6 | open: () => void; 7 | close: () => void; 8 | toggle: () => void; 9 | items: typeof NAVIGATION_ITEMS; 10 | }; 11 | 12 | export const useNavigationStore = create((set) => ({ 13 | isOpen: false, 14 | open: () => set({ isOpen: true }), 15 | close: () => set({ isOpen: false }), 16 | toggle: () => set((state) => ({ isOpen: !state.isOpen })), 17 | items: NAVIGATION_ITEMS, 18 | })); 19 | -------------------------------------------------------------------------------- /docs/src/layouts/main.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Article, Header, Navigation } from "src/components/layout"; 3 | import type { CollectionEntry } from "astro:content"; 4 | import { BASE_URL, SITE } from "src/config/env"; 5 | import "../styles/main.css"; 6 | 7 | type Props = CollectionEntry<"docs">["data"]; 8 | 9 | const { title, description, dir, lang } = Astro.props; 10 | const currentPage = Astro.url.pathname.replace(BASE_URL, "/").replace(/\/$/, ""); 11 | --- 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {`${title} 🚀 ${SITE.title}`} 22 | 23 | 24 | 25 |
    26 |
    27 | 41 | 42 |
    43 |
    44 | 56 | 57 |
    58 | 90 |
    91 |
    92 | 93 | 94 | -------------------------------------------------------------------------------- /docs/src/pages/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { type CollectionEntry, getCollection } from "astro:content"; 3 | import MainLayout from "../layouts/main.astro"; 4 | // import MainLayout from "../layouts/MainLayout.astro"; 5 | // import { Blockquote } from "src/components/Markdown/Blockquote"; 6 | // import { Code as code } from "src/components/Markdown/Code"; 7 | // import Code from "src/components/Markdown/Code2.astro"; 8 | // // import Property from "src/components/Markdown/Property.astro"; 9 | // // import Properties from "src/components/Markdown/Properties.astro"; 10 | import * as components from "src/components/markdown"; 11 | 12 | export async function getStaticPaths() { 13 | const docs = await getCollection("docs"); 14 | return docs.map((entry) => ({ 15 | params: { 16 | slug: entry.slug, 17 | }, 18 | props: entry, 19 | })); 20 | } 21 | type Props = CollectionEntry<"docs">; 22 | 23 | const page = Astro.props; 24 | const { Content } = await page.render(); 25 | --- 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // const { BASE_URL } = import.meta.env; 3 | --- 4 | 5 | 10 | -------------------------------------------------------------------------------- /docs/src/styles/base.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | html { 7 | @apply h-full; 8 | } 9 | body { 10 | @apply h-full; 11 | } 12 | h1 { 13 | @apply text-2xl; 14 | } 15 | h2 { 16 | @apply text-xl; 17 | } 18 | h3 { 19 | @apply text-lg; 20 | } 21 | figure.code-block > figcaption { 22 | @apply absolute z-10 flex items-baseline justify-between px-4 font-mono text-zinc-500; 23 | } 24 | figure.code-block > figcaption span.filename { 25 | @apply pr-8; 26 | } 27 | figure.code-block > figcaption span.lang { 28 | @apply text-xs; 29 | } 30 | figure.code-block > figcaption + div > pre.astro-code { 31 | @apply pt-12; 32 | } 33 | } 34 | 35 | @layer components { 36 | .btn-blue { 37 | @apply rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700; 38 | } 39 | } 40 | 41 | @layer utilities { 42 | .filter-none { 43 | filter: none; 44 | } 45 | .filter-grayscale { 46 | filter: grayscale(100%); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/src/styles/main.css: -------------------------------------------------------------------------------- 1 | @import "./reset.css"; 2 | @import "./base.css"; 3 | 4 | :root { 5 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 6 | line-height: 1.5; 7 | font-weight: 400; 8 | 9 | color-scheme: light dark; 10 | /* color: rgba(255, 255, 255, 0.87); 11 | background-color: #242424; */ 12 | 13 | font-synthesis: none; 14 | text-rendering: optimizeLegibility; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | -webkit-text-size-adjust: 100%; 18 | 19 | --unit-100vh: 100vh; 20 | } 21 | 22 | @supports (height: 100dvh) { 23 | :root { 24 | --unit-100vh: 100dvh; 25 | } 26 | } 27 | 28 | @keyframes electronOrbit { 29 | 0% { 30 | transform: rotate(0deg); 31 | } 32 | 100% { 33 | transform: rotate(360deg); 34 | } 35 | } 36 | @keyframes circle { 37 | to { 38 | transform: rotate(1turn); 39 | } 40 | } 41 | @keyframes updown { 42 | to { 43 | /* (rx - ry) * 2 */ 44 | transform: translateX(-70px); 45 | } 46 | } 47 | .logoElectronContainer { 48 | transform-origin: 0 0; 49 | animation: updown 2s infinite ease-in-out alternate; 50 | } 51 | .logoElectron { 52 | /* (rx - ry) 0 */ 53 | transform-origin: 35px 0; 54 | animation: circle 4s infinite linear; 55 | } 56 | .logoAtom { 57 | transform-origin: 0 0; 58 | animation: circle 8s infinite linear; 59 | } 60 | -------------------------------------------------------------------------------- /docs/src/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | 1. Use a more-intuitive box-sizing model. 3 | */ 4 | *, 5 | *::before, 6 | *::after { 7 | box-sizing: border-box; 8 | } 9 | 10 | /* 11 | 2. Remove default margin 12 | */ 13 | * { 14 | margin: 0; 15 | } 16 | 17 | /* 18 | 3. Allow percentage-based heights in the application 19 | */ 20 | html, 21 | body { 22 | height: 100%; 23 | } 24 | 25 | /* 26 | Typographic tweaks! 27 | 4. Add accessible line-height 28 | 5. Improve text rendering 29 | */ 30 | body { 31 | -webkit-font-smoothing: antialiased; 32 | line-height: 1.5; 33 | } 34 | 35 | /* 36 | 6. Improve media defaults 37 | */ 38 | img, 39 | picture, 40 | video, 41 | canvas, 42 | svg { 43 | display: block; 44 | max-width: 100%; 45 | } 46 | 47 | /* 48 | 7. Remove built-in form typography styles 49 | */ 50 | input, 51 | button, 52 | textarea, 53 | select { 54 | font: inherit; 55 | } 56 | 57 | /* 58 | 8. Avoid text overflows 59 | */ 60 | p, 61 | h1, 62 | h2, 63 | h3, 64 | h4, 65 | h5, 66 | h6 { 67 | overflow-wrap: break-word; 68 | } 69 | 70 | /* 71 | 9. Create a root stacking context 72 | */ 73 | #root { 74 | isolation: isolate; 75 | } 76 | -------------------------------------------------------------------------------- /docs/src/utils/classNames.ts: -------------------------------------------------------------------------------- 1 | export { clsx as classNames } from "clsx"; 2 | -------------------------------------------------------------------------------- /docs/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./classNames"; 2 | export * from "./string"; 3 | -------------------------------------------------------------------------------- /docs/src/utils/string.ts: -------------------------------------------------------------------------------- 1 | export const ucfirst = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 2 | -------------------------------------------------------------------------------- /docs/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | primary: { 8 | 50: "#effbff", 9 | 100: "#dff8fe", 10 | 200: "#c0f0fd", 11 | 300: "#a0e9fd", 12 | 400: "#81e1fc", 13 | 500: "#61dafb", 14 | 600: "#4eaec9", 15 | 700: "#3a8397", 16 | 800: "#275764", 17 | 900: "#132c32", 18 | }, 19 | secondary: { 20 | 50: "#fff9ec", 21 | 100: "#fff4d9", 22 | 200: "#ffe9b4", 23 | 300: "#ffdd8e", 24 | 400: "#ffd269", 25 | 500: "#ffc743", 26 | 600: "#cc9f36", 27 | 700: "#997728", 28 | 800: "#66501b", 29 | 900: "#33280d", 30 | }, 31 | }, 32 | }, 33 | }, 34 | plugins: ["@tailwindcss/typography"], 35 | }; 36 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "react", 6 | "baseUrl": "./", 7 | "paths": { 8 | "src/*": ["src/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import baseConfig from "@mgcrea/eslint-config-react-native"; 4 | 5 | const config = [ 6 | ...baseConfig, 7 | { 8 | languageOptions: { 9 | parserOptions: { 10 | project: true, 11 | tsconfigRootDir: import.meta.dirname, 12 | }, 13 | }, 14 | }, 15 | ]; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {type FunctionComponent} from 'react'; 2 | import {GestureHandlerRootView} from 'react-native-gesture-handler'; 3 | import {DraggableStackExample} from './pages/DraggableStackExample'; 4 | import {SafeAreaView, StyleSheet} from 'react-native'; 5 | import {configureReanimatedLogger} from 'react-native-reanimated'; 6 | 7 | // This is the default configuration 8 | configureReanimatedLogger({ 9 | // level: ReanimatedLogLevel.warn, 10 | strict: false, // Reanimated runs in strict mode by default 11 | }); 12 | 13 | export const App: FunctionComponent = () => { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | const styles = StyleSheet.create({ 24 | container: { 25 | flexGrow: 1, 26 | alignItems: 'stretch', 27 | justifyContent: 'center', 28 | flexDirection: 'row', 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /example/src/pages/DraggableBasicExample.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DndProvider, 3 | DndProviderProps, 4 | doesOverlapHorizontally, 5 | Draggable, 6 | DraggableProps, 7 | Droppable, 8 | DroppableProps, 9 | useDraggableStyle, 10 | useDroppableStyle, 11 | } from '@mgcrea/react-native-dnd/src'; 12 | import React, {useState, type FunctionComponent} from 'react'; 13 | import {SafeAreaView, StyleSheet, Text} from 'react-native'; 14 | import Animated, { 15 | runOnJS, 16 | useSharedValue, 17 | withSpring, 18 | } from 'react-native-reanimated'; 19 | 20 | export const DraggableBasicExample: FunctionComponent = () => { 21 | const [count, setCount] = useState(0); 22 | const dynamicData = useSharedValue({count: 0}); 23 | 24 | const onDragEnd = () => { 25 | setCount(count => count + 1); 26 | }; 27 | 28 | const handleDragEnd: DndProviderProps['onDragEnd'] = ({ 29 | active: _active, 30 | over, 31 | }) => { 32 | 'worklet'; 33 | if (over) { 34 | console.log(`Current count is ${count}`); 35 | runOnJS(onDragEnd)(); 36 | } 37 | }; 38 | 39 | const handleBegin: DndProviderProps['onBegin'] = () => { 40 | 'worklet'; 41 | console.log('onBegin'); 42 | }; 43 | 44 | const handleFinalize: DndProviderProps['onFinalize'] = () => { 45 | 'worklet'; 46 | console.log('onFinalize'); 47 | }; 48 | 49 | const shouldDropWorklet: DndProviderProps['shouldDropWorklet'] = ( 50 | active, 51 | item, 52 | ) => { 53 | 'worklet'; 54 | return doesOverlapHorizontally(active, item); 55 | }; 56 | 57 | return ( 58 | 59 | 64 | 65 | DROP 66 | 67 | 68 | DRAG 69 | 70 | count is {count} 71 | 72 | 73 | ); 74 | }; 75 | 76 | const MyDraggable: FunctionComponent = ({ 77 | id, 78 | ...otherProps 79 | }) => { 80 | const animatedStyle = useDraggableStyle(id, ({isActive, isActing}) => { 81 | 'worklet'; 82 | return { 83 | opacity: isActing ? 0.5 : 1, 84 | backgroundColor: isActive ? 'lightseagreen' : 'seagreen', 85 | transform: [{scale: withSpring(isActive ? 1.1 : 1)}], 86 | }; 87 | }); 88 | 89 | return ( 90 | 91 | 92 | DRAG 93 | 94 | 95 | ); 96 | }; 97 | 98 | const MyDroppable: FunctionComponent = ({ 99 | id, 100 | ...otherProps 101 | }) => { 102 | const animatedStyle = useDroppableStyle(id, ({isActive}) => { 103 | 'worklet'; 104 | return { 105 | opacity: isActive ? 0.9 : 1, 106 | backgroundColor: isActive ? 'steelblue' : 'teal', 107 | transform: [{rotate: withSpring(isActive ? `-20deg` : `0deg`)}], 108 | }; 109 | }); 110 | 111 | return ( 112 | 113 | 114 | DROP 115 | 116 | 117 | ); 118 | }; 119 | 120 | const styles = StyleSheet.create({ 121 | container: { 122 | alignItems: 'center', 123 | justifyContent: 'center', 124 | borderColor: 'black', 125 | borderWidth: StyleSheet.hairlineWidth, 126 | borderRadius: 32, 127 | padding: 32, 128 | backgroundColor: 'rgba(0,0,0,0.1)', 129 | }, 130 | box: { 131 | margin: 24, 132 | padding: 12, 133 | height: 96, 134 | width: 96, 135 | borderRadius: 8, 136 | justifyContent: 'center', 137 | alignItems: 'center', 138 | }, 139 | text: { 140 | color: 'white', 141 | fontWeight: 'bold', 142 | textShadowColor: 'rgba(0, 0, 0, .4)', 143 | textShadowOffset: {width: 1, height: 1}, 144 | textShadowRadius: 14, 145 | padding: 12, 146 | }, 147 | }); 148 | -------------------------------------------------------------------------------- /example/src/pages/DraggableGridExample.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DndProvider, 3 | type ObjectWithId, 4 | Draggable, 5 | DraggableGrid, 6 | DraggableGridProps, 7 | } from '@mgcrea/react-native-dnd'; 8 | import {type FunctionComponent} from 'react'; 9 | import {StyleSheet, Text, View} from 'react-native'; 10 | 11 | const GRID_SIZE = 3; 12 | const items: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; 13 | const data = items.map((letter, index) => ({ 14 | id: `${index}-${letter}`, 15 | value: letter, 16 | })) satisfies ObjectWithId[]; 17 | 18 | export const DraggableGridExample: FunctionComponent = () => { 19 | const onGridOrderChange: DraggableGridProps['onOrderChange'] = value => { 20 | console.log('onGridOrderChange', value); 21 | }; 22 | 23 | return ( 24 | 25 | DraggableGrid{'\n'}Example 26 | 27 | 32 | {data.map(item => ( 33 | 34 | {item.value} 35 | 36 | ))} 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | const LETTER_WIDTH = 100; 44 | const LETTER_HEIGHT = 100; 45 | const LETTER_GAP = 10; 46 | 47 | const styles = StyleSheet.create({ 48 | container: { 49 | flexGrow: 1, 50 | alignItems: 'center', 51 | justifyContent: 'center', 52 | }, 53 | grid: { 54 | backgroundColor: 'rgba(0,0,0,0.1)', 55 | alignItems: 'center', 56 | justifyContent: 'flex-start', 57 | width: LETTER_WIDTH * GRID_SIZE + LETTER_GAP * (GRID_SIZE - 1), 58 | gap: LETTER_GAP, 59 | borderRadius: 32, 60 | }, 61 | title: { 62 | color: '#555', 63 | textTransform: 'uppercase', 64 | fontWeight: 'bold', 65 | position: 'absolute', 66 | top: 10, 67 | left: 10, 68 | }, 69 | draggable: { 70 | backgroundColor: 'seagreen', 71 | width: LETTER_WIDTH, 72 | height: LETTER_HEIGHT, 73 | borderColor: 'rgba(0,0,0,0.2)', 74 | borderWidth: 1, 75 | overflow: 'hidden', 76 | alignItems: 'center', 77 | justifyContent: 'center', 78 | borderRadius: 32, 79 | }, 80 | text: { 81 | color: 'white', 82 | fontWeight: 'bold', 83 | fontSize: 32, 84 | }, 85 | }); 86 | -------------------------------------------------------------------------------- /example/src/pages/DraggableStackExample.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DndProvider, 3 | Draggable, 4 | DraggableStack, 5 | type DraggableStackHandle, 6 | type UniqueIdentifier, 7 | type DraggableStackProps, 8 | type ObjectWithId, 9 | } from '@mgcrea/react-native-dnd/src'; 10 | import React, {useState, type FunctionComponent} from 'react'; 11 | import {Button, StyleSheet, Text, View} from 'react-native'; 12 | import {runOnUI} from 'react-native-reanimated'; 13 | 14 | const items = ['🤓', '🤖🤖', '👻👻👻', '👾👾👾👾']; 15 | const data = items.map((letter, index) => ({ 16 | value: letter, 17 | id: `${index}-${letter}`, 18 | })) satisfies ObjectWithId[]; 19 | let id = items.length; 20 | 21 | export const DraggableStackExample: FunctionComponent = () => { 22 | const [items, setItems] = useState(data); 23 | const [fontSize, setFontSize] = useState(32); 24 | const ref = React.useRef(null); 25 | 26 | const onStackOrderChange: DraggableStackProps['onOrderChange'] = ( 27 | order: UniqueIdentifier[], 28 | ) => { 29 | console.log('onStackOrderChange', order); 30 | }; 31 | const onStackOrderUpdate: DraggableStackProps['onOrderUpdate'] = value => { 32 | console.log('onStackOrderUpdate', value); 33 | }; 34 | 35 | return ( 36 | 37 | DraggableStack Example 38 | 39 | 46 | {items.map((letter, index) => ( 47 | 51 | {letter.value} 52 | 53 | ))} 54 | 55 | 56 | 57 |