react-native-morph-card 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/android/build.gradle +59 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/com/melivalesca/morphcard/MorphCardModule.kt +120 -0
  6. package/android/src/main/java/com/melivalesca/morphcard/MorphCardPackage.kt +42 -0
  7. package/android/src/main/java/com/melivalesca/morphcard/MorphCardSourceManager.kt +40 -0
  8. package/android/src/main/java/com/melivalesca/morphcard/MorphCardSourceView.kt +755 -0
  9. package/android/src/main/java/com/melivalesca/morphcard/MorphCardTargetManager.kt +48 -0
  10. package/android/src/main/java/com/melivalesca/morphcard/MorphCardTargetView.kt +159 -0
  11. package/android/src/main/java/com/melivalesca/morphcard/MorphCardViewRegistry.kt +24 -0
  12. package/android/src/main/jni/CMakeLists.txt +62 -0
  13. package/common/cpp/react/renderer/components/morphcard/RNCMorphCardState.h +30 -0
  14. package/ios/Fabric/RNCMorphCardSourceComponentView.h +25 -0
  15. package/ios/Fabric/RNCMorphCardSourceComponentView.mm +582 -0
  16. package/ios/Fabric/RNCMorphCardTargetComponentView.h +20 -0
  17. package/ios/Fabric/RNCMorphCardTargetComponentView.mm +99 -0
  18. package/ios/RNCMorphCardModule.h +14 -0
  19. package/ios/RNCMorphCardModule.mm +126 -0
  20. package/ios/RNCMorphCardSource.h +23 -0
  21. package/ios/RNCMorphCardSource.m +144 -0
  22. package/ios/RNCMorphCardSourceManager.h +5 -0
  23. package/ios/RNCMorphCardSourceManager.m +17 -0
  24. package/ios/RNCMorphCardTarget.h +19 -0
  25. package/ios/RNCMorphCardTarget.m +27 -0
  26. package/ios/RNCMorphCardTargetManager.h +5 -0
  27. package/ios/RNCMorphCardTargetManager.m +16 -0
  28. package/ios/RNCMorphCardViewRegistry.h +35 -0
  29. package/ios/RNCMorphCardViewRegistry.m +40 -0
  30. package/lib/commonjs/MorphCard.types.js +6 -0
  31. package/lib/commonjs/MorphCard.types.js.map +1 -0
  32. package/lib/commonjs/MorphCardSource.js +95 -0
  33. package/lib/commonjs/MorphCardSource.js.map +1 -0
  34. package/lib/commonjs/MorphCardTarget.js +83 -0
  35. package/lib/commonjs/MorphCardTarget.js.map +1 -0
  36. package/lib/commonjs/index.js +45 -0
  37. package/lib/commonjs/index.js.map +1 -0
  38. package/lib/commonjs/package.json +1 -0
  39. package/lib/commonjs/specs/NativeMorphCardModule.js +9 -0
  40. package/lib/commonjs/specs/NativeMorphCardModule.js.map +1 -0
  41. package/lib/commonjs/specs/NativeMorphCardSource.js +10 -0
  42. package/lib/commonjs/specs/NativeMorphCardSource.js.map +1 -0
  43. package/lib/commonjs/specs/NativeMorphCardTarget.js +10 -0
  44. package/lib/commonjs/specs/NativeMorphCardTarget.js.map +1 -0
  45. package/lib/commonjs/useMorphTarget.js +28 -0
  46. package/lib/commonjs/useMorphTarget.js.map +1 -0
  47. package/lib/module/MorphCard.types.js +4 -0
  48. package/lib/module/MorphCard.types.js.map +1 -0
  49. package/lib/module/MorphCardSource.js +85 -0
  50. package/lib/module/MorphCardSource.js.map +1 -0
  51. package/lib/module/MorphCardTarget.js +76 -0
  52. package/lib/module/MorphCardTarget.js.map +1 -0
  53. package/lib/module/index.js +6 -0
  54. package/lib/module/index.js.map +1 -0
  55. package/lib/module/package.json +1 -0
  56. package/lib/module/specs/NativeMorphCardModule.js +5 -0
  57. package/lib/module/specs/NativeMorphCardModule.js.map +1 -0
  58. package/lib/module/specs/NativeMorphCardSource.js +5 -0
  59. package/lib/module/specs/NativeMorphCardSource.js.map +1 -0
  60. package/lib/module/specs/NativeMorphCardTarget.js +5 -0
  61. package/lib/module/specs/NativeMorphCardTarget.js.map +1 -0
  62. package/lib/module/useMorphTarget.js +22 -0
  63. package/lib/module/useMorphTarget.js.map +1 -0
  64. package/lib/typescript/src/MorphCard.types.d.ts +29 -0
  65. package/lib/typescript/src/MorphCard.types.d.ts.map +1 -0
  66. package/lib/typescript/src/MorphCardSource.d.ts +35 -0
  67. package/lib/typescript/src/MorphCardSource.d.ts.map +1 -0
  68. package/lib/typescript/src/MorphCardTarget.d.ts +20 -0
  69. package/lib/typescript/src/MorphCardTarget.d.ts.map +1 -0
  70. package/lib/typescript/src/index.d.ts +6 -0
  71. package/lib/typescript/src/index.d.ts.map +1 -0
  72. package/lib/typescript/src/specs/NativeMorphCardModule.d.ts +14 -0
  73. package/lib/typescript/src/specs/NativeMorphCardModule.d.ts.map +1 -0
  74. package/lib/typescript/src/specs/NativeMorphCardSource.d.ts +13 -0
  75. package/lib/typescript/src/specs/NativeMorphCardSource.d.ts.map +1 -0
  76. package/lib/typescript/src/specs/NativeMorphCardTarget.d.ts +25 -0
  77. package/lib/typescript/src/specs/NativeMorphCardTarget.d.ts.map +1 -0
  78. package/lib/typescript/src/useMorphTarget.d.ts +16 -0
  79. package/lib/typescript/src/useMorphTarget.d.ts.map +1 -0
  80. package/package.json +101 -0
  81. package/react-native-morph-card.podspec +41 -0
  82. package/react-native.config.js +13 -0
  83. package/src/MorphCard.types.ts +29 -0
  84. package/src/MorphCardSource.tsx +105 -0
  85. package/src/MorphCardTarget.tsx +127 -0
  86. package/src/index.tsx +10 -0
  87. package/src/specs/NativeMorphCardModule.ts +21 -0
  88. package/src/specs/NativeMorphCardSource.ts +20 -0
  89. package/src/specs/NativeMorphCardTarget.ts +38 -0
  90. package/src/useMorphTarget.ts +21 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MeliValesca
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 all
13
+ 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 THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # react-native-morph-card
2
+
3
+ Native card-to-modal morph transition for React Native. The iOS App Store "card of the day" expand animation, as a library.
4
+
5
+ - Native animations on both platforms (UIKit `UIViewPropertyAnimator` / Android Transition framework)
6
+ - No JS-driven animation, no webview, no experimental flags
7
+ - Works with any navigation setup
8
+ - Supports old and new React Native architecture (Paper + Fabric)
9
+
10
+ > **Status:** Early development — the skeleton is in place, native morph animation is not yet implemented.
11
+
12
+ ## Installation
13
+
14
+ ```sh
15
+ yarn add react-native-morph-card
16
+ ```
17
+
18
+ ### iOS
19
+
20
+ ```sh
21
+ cd ios && pod install
22
+ ```
23
+
24
+ ### Android
25
+
26
+ No additional steps required.
27
+
28
+ ## Usage
29
+
30
+ ```tsx
31
+ import { MorphCardSource } from 'react-native-morph-card';
32
+
33
+ function App() {
34
+ return (
35
+ <MorphCardSource style={styles.card}>
36
+ <Pressable onPress={() => console.log('Morph!')}>
37
+ <Text>Tap me</Text>
38
+ </Pressable>
39
+ </MorphCardSource>
40
+ );
41
+ }
42
+ ```
43
+
44
+ ## Running the example app
45
+
46
+ The example app lives in the `example/` directory and uses [`react-native-test-app`](https://github.com/nicklasmoeller/react-native-test-app) to manage the native projects.
47
+
48
+ ### Prerequisites
49
+
50
+ | Tool | Version |
51
+ |------|---------|
52
+ | Node.js | >= 18 |
53
+ | Yarn | 1.x |
54
+ | Ruby | >= 2.7 (for CocoaPods) |
55
+ | CocoaPods | ~> 1.15 |
56
+ | Xcode | >= 15 (iOS) |
57
+ | Android Studio | latest (Android) |
58
+ | JDK | 17 |
59
+
60
+ Make sure you have an iOS Simulator or Android Emulator available.
61
+
62
+ ### 1. Install dependencies
63
+
64
+ From the repository root:
65
+
66
+ ```sh
67
+ # Install root (library) dependencies
68
+ yarn install
69
+
70
+ # Install example app dependencies
71
+ cd example
72
+ yarn install
73
+ ```
74
+
75
+ ### 2. Run on iOS
76
+
77
+ ```sh
78
+ # Install CocoaPods (from example/ios)
79
+ cd ios
80
+ bundle install
81
+ bundle exec pod install
82
+ cd ..
83
+
84
+ # Start Metro and build
85
+ yarn ios
86
+ ```
87
+
88
+ This opens the app on the iOS Simulator. Metro starts automatically.
89
+
90
+ If you prefer to build from Xcode, open `example/ios/MorphCardExample.xcworkspace` and press Run.
91
+
92
+ ### 3. Run on Android
93
+
94
+ Make sure an Android emulator is running (or a device is connected), then from `example/`:
95
+
96
+ ```sh
97
+ yarn android
98
+ ```
99
+
100
+ Gradle will download dependencies on first run — this can take a few minutes.
101
+
102
+ ### 4. Start Metro separately (optional)
103
+
104
+ If you want to start the Metro bundler on its own (e.g. to see logs in a dedicated terminal):
105
+
106
+ ```sh
107
+ cd example
108
+ yarn start
109
+ ```
110
+
111
+ Then run `yarn ios` or `yarn android` in another terminal.
112
+
113
+ ### Building JS bundles manually
114
+
115
+ Only needed if you want to test pre-built bundles (not required for normal development):
116
+
117
+ ```sh
118
+ cd example
119
+ yarn build:ios
120
+ yarn build:android
121
+ ```
122
+
123
+ ### Troubleshooting
124
+
125
+ | Problem | Fix |
126
+ |---------|-----|
127
+ | `pod install` fails | Run `bundle install` first, then `bundle exec pod install` |
128
+ | Android build fails on first run | Make sure `ANDROID_HOME` is set and an emulator/device is available |
129
+ | Metro can't find `react-native-morph-card` | Run `yarn install` at the repo root first |
130
+ | Duplicate module errors | Delete `node_modules` in both root and `example/`, then reinstall |
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,59 @@
1
+ buildscript {
2
+ ext.safeExtGet = { prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+ }
6
+
7
+ apply plugin: 'com.android.library'
8
+ apply plugin: 'kotlin-android'
9
+
10
+ def newArchEnabled = project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
11
+
12
+ android {
13
+ namespace "com.melivalesca.morphcard"
14
+ compileSdkVersion safeExtGet('compileSdkVersion', 34)
15
+
16
+ defaultConfig {
17
+ minSdkVersion safeExtGet('minSdkVersion', 21)
18
+ targetSdkVersion safeExtGet('targetSdkVersion', 34)
19
+ }
20
+
21
+ sourceSets.main {
22
+ java {
23
+ if (newArchEnabled) {
24
+ srcDirs += 'src/fabric/java'
25
+ } else {
26
+ srcDirs += 'src/paper/java'
27
+ }
28
+ }
29
+ }
30
+
31
+ lintOptions {
32
+ abortOnError false
33
+ }
34
+ }
35
+
36
+ repositories {
37
+ mavenCentral()
38
+ google()
39
+ }
40
+
41
+ dependencies {
42
+ implementation "com.facebook.react:react-native:+"
43
+ }
44
+
45
+ if (newArchEnabled) {
46
+ apply plugin: "com.facebook.react"
47
+ }
48
+
49
+ try {
50
+ apply plugin: 'com.diffplug.spotless'
51
+ spotless {
52
+ kotlin {
53
+ target '**/*.kt'
54
+ googleJavaFormat()
55
+ }
56
+ }
57
+ } catch (Exception ignored) {
58
+ // Spotless plugin not available in consumer projects
59
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.melivalesca.morphcard">
3
+ </manifest>
@@ -0,0 +1,120 @@
1
+ package com.melivalesca.morphcard
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.view.View
6
+ import com.facebook.react.bridge.Promise
7
+ import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.bridge.WritableNativeMap
9
+
10
+ class MorphCardModule(reactContext: ReactApplicationContext) :
11
+ NativeMorphCardModuleSpec(reactContext) {
12
+
13
+ private val mainHandler = Handler(Looper.getMainLooper())
14
+
15
+ override fun prepareExpand(sourceTag: Double) {
16
+ mainHandler.post {
17
+ val sourceView = MorphCardViewRegistry.getView(sourceTag.toInt())
18
+ if (sourceView is MorphCardSourceView) {
19
+ sourceView.prepareExpand(null)
20
+ }
21
+ }
22
+ }
23
+
24
+ override fun setTargetConfig(
25
+ sourceTag: Double,
26
+ targetWidth: Double,
27
+ targetHeight: Double,
28
+ targetBorderRadius: Double,
29
+ contentOffsetY: Double,
30
+ contentCentered: Boolean
31
+ ) {
32
+ mainHandler.post {
33
+ val sourceView = MorphCardViewRegistry.getView(sourceTag.toInt())
34
+ if (sourceView is MorphCardSourceView) {
35
+ sourceView.pendingTargetWidth = targetWidth.toFloat()
36
+ sourceView.pendingTargetHeight = targetHeight.toFloat()
37
+ sourceView.pendingTargetBorderRadius = targetBorderRadius.toFloat()
38
+ sourceView.pendingContentOffsetY = contentOffsetY.toFloat()
39
+ sourceView.pendingContentCentered = contentCentered
40
+ }
41
+ }
42
+ }
43
+
44
+ override fun expand(sourceTag: Double, targetTag: Double, promise: Promise) {
45
+ mainHandler.post {
46
+ val sourceView = MorphCardViewRegistry.getView(sourceTag.toInt())
47
+ val targetView = MorphCardViewRegistry.getView(targetTag.toInt())
48
+
49
+ if (sourceView is MorphCardSourceView) {
50
+ // If prepareExpand wasn't called yet (e.g. via morphExpand API),
51
+ // create the overlay now.
52
+ if (!sourceView.hasOverlay) {
53
+ sourceView.prepareExpand(targetView)
54
+ }
55
+
56
+ // Wait until the target view is registered AND its
57
+ // screen container is ready, then animate.
58
+ animateWhenReady(sourceView, targetTag.toInt(), promise, 0)
59
+ } else {
60
+ promise.resolve(false)
61
+ }
62
+ }
63
+ }
64
+
65
+ private fun animateWhenReady(
66
+ sourceView: MorphCardSourceView,
67
+ targetTag: Int,
68
+ promise: Promise,
69
+ attempt: Int
70
+ ) {
71
+ val targetView = MorphCardViewRegistry.getView(targetTag)
72
+
73
+ // Wait if: target not registered yet, OR screen container not found
74
+ val needsWait = targetView == null || !sourceView.isTargetScreenReady(targetView)
75
+ if (needsWait && attempt < 20) {
76
+ mainHandler.postDelayed({
77
+ animateWhenReady(sourceView, targetTag, promise, attempt + 1)
78
+ }, 50)
79
+ return
80
+ }
81
+
82
+ // Re-hide target screen if we found the target late
83
+ if (targetView != null) {
84
+ sourceView.hideTargetScreen(targetView)
85
+ }
86
+
87
+ sourceView.animateExpand(targetView, promise)
88
+ }
89
+
90
+ override fun collapse(sourceTag: Double, promise: Promise) {
91
+ mainHandler.post {
92
+ val sourceView = MorphCardViewRegistry.getView(sourceTag.toInt())
93
+ if (sourceView is MorphCardSourceView) {
94
+ sourceView.collapseWithResolve(promise)
95
+ } else {
96
+ promise.resolve(false)
97
+ }
98
+ }
99
+ }
100
+
101
+ override fun getSourceSize(sourceTag: Double, promise: Promise) {
102
+ mainHandler.post {
103
+ val sourceView = MorphCardViewRegistry.getView(sourceTag.toInt())
104
+ val map = WritableNativeMap()
105
+ if (sourceView != null) {
106
+ val density = sourceView.resources.displayMetrics.density
107
+ map.putDouble("width", (sourceView.width / density).toDouble())
108
+ map.putDouble("height", (sourceView.height / density).toDouble())
109
+ } else {
110
+ map.putDouble("width", 0.0)
111
+ map.putDouble("height", 0.0)
112
+ }
113
+ promise.resolve(map)
114
+ }
115
+ }
116
+
117
+ companion object {
118
+ const val NAME = "RNCMorphCardModule"
119
+ }
120
+ }
@@ -0,0 +1,42 @@
1
+ package com.melivalesca.morphcard
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+
10
+ class MorphCardPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return when (name) {
13
+ MorphCardModule.NAME -> MorphCardModule(reactContext)
14
+ else -> null
15
+ }
16
+ }
17
+
18
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
19
+ return ReactModuleInfoProvider {
20
+ mapOf(
21
+ MorphCardModule.NAME to
22
+ ReactModuleInfo(
23
+ MorphCardModule.NAME,
24
+ MorphCardModule.NAME,
25
+ false, // canOverrideExistingModule
26
+ false, // needsEagerInit
27
+ false, // isCxxModule
28
+ true, // isTurboModule
29
+ )
30
+ )
31
+ }
32
+ }
33
+
34
+ override fun createViewManagers(
35
+ reactContext: ReactApplicationContext
36
+ ): List<ViewManager<*, *>> {
37
+ return listOf(
38
+ MorphCardSourceManager(),
39
+ MorphCardTargetManager(),
40
+ )
41
+ }
42
+ }
@@ -0,0 +1,40 @@
1
+ package com.melivalesca.morphcard
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.ThemedReactContext
5
+ import com.facebook.react.uimanager.ViewGroupManager
6
+ import com.facebook.react.uimanager.ViewManagerDelegate
7
+ import com.facebook.react.viewmanagers.RNCMorphCardSourceManagerDelegate
8
+ import com.facebook.react.viewmanagers.RNCMorphCardSourceManagerInterface
9
+
10
+ @ReactModule(name = MorphCardSourceManager.REACT_CLASS)
11
+ class MorphCardSourceManager :
12
+ ViewGroupManager<MorphCardSourceView>(),
13
+ RNCMorphCardSourceManagerInterface<MorphCardSourceView> {
14
+
15
+ private val delegate = RNCMorphCardSourceManagerDelegate(this)
16
+
17
+ override fun getDelegate(): ViewManagerDelegate<MorphCardSourceView> = delegate
18
+
19
+ override fun getName(): String = REACT_CLASS
20
+
21
+ override fun createViewInstance(reactContext: ThemedReactContext): MorphCardSourceView {
22
+ return MorphCardSourceView(reactContext)
23
+ }
24
+
25
+ override fun setDuration(view: MorphCardSourceView, value: Double) {
26
+ view.duration = value
27
+ }
28
+
29
+ override fun setScaleMode(view: MorphCardSourceView, value: String?) {
30
+ view.scaleMode = value ?: "aspectFill"
31
+ }
32
+
33
+ override fun setCardBorderRadius(view: MorphCardSourceView, value: Double) {
34
+ view.borderRadiusDp = value.toFloat()
35
+ }
36
+
37
+ companion object {
38
+ const val REACT_CLASS = "RNCMorphCardSource"
39
+ }
40
+ }