├── hooks
├── useColorScheme.ts
├── useColorScheme.web.ts
└── useThemeColor.ts
├── assets
├── images
│ ├── dp.png
│ ├── icon.png
│ ├── favicon.png
│ ├── gift_box.webp
│ ├── react-logo.png
│ ├── yellow_star.webp
│ ├── adaptive-icon.png
│ ├── react-logo@2x.png
│ ├── react-logo@3x.png
│ ├── twitter-cover.png
│ ├── splash-icon-dark.png
│ ├── splash-icon-light.png
│ └── partial-react-logo.png
├── fonts
│ ├── Inter-Bold.ttf
│ ├── Inter-Medium.ttf
│ ├── Inter-Regular.ttf
│ └── Inter-SemiBold.ttf
└── icon
│ └── slack-chevron-left.png
├── app
├── wa-status
│ ├── index.android.tsx
│ ├── notes.tsx
│ ├── index.tsx
│ ├── page.tsx
│ ├── _layout.tsx
│ └── index.ios.tsx
├── untitled
│ ├── _layout.android.tsx
│ ├── _layout.tsx
│ ├── index.tsx
│ ├── [id].tsx
│ └── _layout.ios.tsx
├── slack-liquid-glass
│ ├── index.android.tsx
│ ├── index.tsx
│ ├── _layout.tsx
│ ├── chat.tsx
│ └── index.ios.tsx
├── 3d-rotate
│ ├── _layout.tsx
│ ├── index.tsx
│ └── default.tsx
├── stacked-input
│ └── _layout.tsx
├── 3d-card-scrolling
│ ├── _layout.tsx
│ └── index.tsx
├── safari-bar
│ ├── _layout.tsx
│ └── index.tsx
├── open-gift
│ ├── _layout.tsx
│ └── index.tsx
├── re-captcha
│ ├── _layout.tsx
│ └── index.tsx
├── in-app-notif
│ ├── _layout.tsx
│ └── index.tsx
├── slider-demo
│ ├── _layout.tsx
│ └── index.tsx
├── twitter-profile
│ └── _layout.tsx
├── threads-pull-refresh
│ ├── _layout.tsx
│ └── index.tsx
├── keyboard-ctrl
│ ├── index.tsx
│ └── _layout.tsx
├── gesture-menu
│ ├── _layout.tsx
│ └── index.tsx
├── gesture-nav
│ ├── _layout.tsx
│ └── index.tsx
├── grok-sidebar
│ └── _layout.tsx
├── ios-camera
│ ├── _layout.tsx
│ └── index.tsx
├── shared-option
│ ├── _layout.tsx
│ └── expanded.tsx
├── index.tsx
└── _layout.tsx
├── constants
├── config.json
├── Theme.ts
├── Colors.ts
└── index.ts
├── components
├── untitled
│ ├── screen.tsx
│ ├── header.tsx
│ ├── screen.ios.tsx
│ ├── button.tsx
│ ├── header.ios.tsx
│ ├── card.tsx
│ └── bottom-bar.tsx
├── stacked-input
│ ├── index.tsx
│ ├── switch.tsx
│ ├── provider.tsx
│ ├── trigger.tsx
│ └── input.tsx
├── re-captcha
│ ├── config.ts
│ ├── underlay-text.tsx
│ ├── shimmer.tsx
│ ├── card.tsx
│ └── tray.tsx
├── ui
│ ├── Logo.tsx
│ ├── dots.tsx
│ ├── animated-text.tsx
│ ├── TextLink.tsx
│ ├── Button.tsx
│ ├── glassy-view.tsx
│ └── check-box.tsx
├── notify
│ ├── type.ts
│ ├── index.tsx
│ └── card.tsx
├── slack
│ ├── header.tsx
│ ├── config.ts
│ ├── banner.tsx
│ ├── message-box.tsx
│ ├── message-bar.tsx
│ ├── header.ios.tsx
│ └── island.tsx
├── Container.tsx
├── ResponsiveText.tsx
├── safari-bar
│ ├── article.tsx
│ ├── header.tsx
│ ├── config.ts
│ └── bar.tsx
├── ProgressiveFade.tsx
├── ThemedView.tsx
├── PresableBounce.tsx
├── HeadComponent.tsx
├── ThemedText.tsx
├── icons
│ └── index.tsx
├── drawer
│ ├── DrawerItemList.tsx
│ └── DrawerItem.tsx
├── 3dRotate.tsx
├── DrawerContent.tsx
├── RefreshLogo.tsx
├── TextArea.tsx
└── Slider.tsx
├── tsconfig.json
├── eas.json
├── .gitignore
├── functions
└── index.ts
├── README.md
├── app.json
└── package.json
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | export { useColorScheme } from 'react-native';
2 |
--------------------------------------------------------------------------------
/assets/images/dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/dp.png
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/icon.png
--------------------------------------------------------------------------------
/app/wa-status/index.android.tsx:
--------------------------------------------------------------------------------
1 | export * from "./index.ios";
2 | export { default } from "./index.ios";
3 |
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/favicon.png
--------------------------------------------------------------------------------
/app/untitled/_layout.android.tsx:
--------------------------------------------------------------------------------
1 | export * from "./_layout.ios";
2 | export { default } from "./_layout.ios";
3 |
--------------------------------------------------------------------------------
/assets/fonts/Inter-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/fonts/Inter-Bold.ttf
--------------------------------------------------------------------------------
/assets/images/gift_box.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/gift_box.webp
--------------------------------------------------------------------------------
/app/slack-liquid-glass/index.android.tsx:
--------------------------------------------------------------------------------
1 | export * from "./index.ios";
2 | export { default } from "./index.ios";
3 |
--------------------------------------------------------------------------------
/assets/fonts/Inter-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/fonts/Inter-Medium.ttf
--------------------------------------------------------------------------------
/assets/fonts/Inter-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/fonts/Inter-Regular.ttf
--------------------------------------------------------------------------------
/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/react-logo.png
--------------------------------------------------------------------------------
/assets/images/yellow_star.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/yellow_star.webp
--------------------------------------------------------------------------------
/assets/fonts/Inter-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/fonts/Inter-SemiBold.ttf
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/assets/images/twitter-cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/twitter-cover.png
--------------------------------------------------------------------------------
/assets/icon/slack-chevron-left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/icon/slack-chevron-left.png
--------------------------------------------------------------------------------
/assets/images/splash-icon-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/splash-icon-dark.png
--------------------------------------------------------------------------------
/assets/images/splash-icon-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/splash-icon-light.png
--------------------------------------------------------------------------------
/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Solarin-Johnson/my-app/HEAD/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/app/3d-rotate/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 |
3 | export default function Layout() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/stacked-input/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 |
3 | export default function Layout() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/3d-card-scrolling/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 |
3 | export default function Layout() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/wa-status/notes.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { View } from "react-native";
3 |
4 | export default function Page() {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/app/safari-bar/_layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Slot } from "expo-router";
3 |
4 | export default function Layout() {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/app/open-gift/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 |
3 | export default function Layout() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/re-captcha/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 |
3 | export default function Layout() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/in-app-notif/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 |
3 | export default function Layout() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/slider-demo/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 |
3 | export default function Layout() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/twitter-profile/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 |
3 | export default function Layout() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/threads-pull-refresh/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 |
3 | export default function Layout() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/constants/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Solarin",
3 | "contact": {
4 | "twitter": "https://x.com/S0LARIN/",
5 | "github": "https://github.com/Solarin-Johnson"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/keyboard-ctrl/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BottomBar from "../../components/BottomBar";
3 |
4 | export default function KeyboardCtrl() {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/components/untitled/screen.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from "react-native";
2 | import React from "react";
3 |
4 | export default function UntitledScreen(props: any) {
5 | return null;
6 | }
7 |
--------------------------------------------------------------------------------
/app/gesture-menu/_layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Slot } from "expo-router";
3 |
4 | export default function Layout() {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/app/gesture-nav/_layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Slot } from "expo-router";
3 |
4 | export default function Layout() {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/app/grok-sidebar/_layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Slot } from "expo-router";
3 |
4 | export default function Layout() {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/app/ios-camera/_layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Slot } from "expo-router";
3 |
4 | export default function Layout() {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/app/keyboard-ctrl/_layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Slot } from "expo-router";
3 |
4 | export default function Layout() {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/app/slack-liquid-glass/index.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from "react-native";
2 | import React from "react";
3 |
4 | export default function Index() {
5 | return (
6 |
7 | Index
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/components/stacked-input/index.tsx:
--------------------------------------------------------------------------------
1 | import Input from "./input";
2 | import { Provider } from "./provider";
3 | import Switch from "./switch";
4 | import Trigger from "./trigger";
5 |
6 | export const Stacked = {
7 | Provider,
8 | Input,
9 | Trigger,
10 | Switch,
11 | };
12 |
--------------------------------------------------------------------------------
/app/wa-status/index.tsx:
--------------------------------------------------------------------------------
1 | export default function Index() {
2 | return null;
3 | }
4 |
5 | // Dummy components to ignore errors
6 | export const ClusterContainer = (props: any) => null;
7 | export const ClusterItem = (props: any) => null;
8 | export const PageContainer = (props: any) => null;
9 |
--------------------------------------------------------------------------------
/components/re-captcha/config.ts:
--------------------------------------------------------------------------------
1 | import { SharedValue } from "react-native-reanimated";
2 |
3 | export const SPRING_CONFIG = {
4 | damping: 10,
5 | stiffness: 90,
6 | mass: 0.5,
7 | };
8 |
9 | export type ReCaptchaProps = {
10 | shrinkProgress: SharedValue;
11 | isVerified: SharedValue;
12 | };
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": [
7 | "./*"
8 | ]
9 | }
10 | },
11 | "include": [
12 | "**/*.ts",
13 | "**/*.tsx",
14 | ".expo/types/**/*.ts",
15 | "expo-env.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/components/ui/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { View } from "react-native";
2 | import React from "react";
3 | import { LogoIcon } from "../icons";
4 | import { useThemeColor } from "@/hooks/useThemeColor";
5 |
6 | export default function Logo() {
7 | const text = useThemeColor("text");
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/app/wa-status/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ClusterContainer, ClusterItem, PageContainer } from "./index";
3 |
4 | export default function Page() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 16.8.0",
4 | "appVersionSource": "remote"
5 | },
6 | "build": {
7 | "development": {
8 | "developmentClient": true,
9 | "distribution": "internal"
10 | },
11 | "preview": {
12 | "distribution": "internal"
13 | },
14 | "production": {
15 | "autoIncrement": true
16 | }
17 | },
18 | "submit": {
19 | "production": {}
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/components/notify/type.ts:
--------------------------------------------------------------------------------
1 | export type MessageOptions = {
2 | description?: string;
3 | expandedChild?: React.ReactNode;
4 | action?: {
5 | label: string;
6 | onClick: () => void;
7 | };
8 | };
9 |
10 | export type MessageType = {
11 | id?: number;
12 | text: string;
13 | options?: MessageOptions;
14 | };
15 |
16 | export interface NotifyContextType {
17 | notify: (msg: string, options?: MessageOptions) => void;
18 | messages: MessageType[];
19 | }
20 |
--------------------------------------------------------------------------------
/components/slack/header.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from "react-native";
2 | import React from "react";
3 |
4 | export default function HeaderTitle({}: {
5 | children: React.ReactNode;
6 | tintColor?: string;
7 | style?: any;
8 | }) {
9 | return (
10 |
11 | Header
12 |
13 | );
14 | }
15 |
16 | export const HeaderRight = () => {
17 | return (
18 |
19 | Right
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/untitled/header.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleProp, ViewStyle } from "react-native";
2 | import React from "react";
3 | export interface UntitledHeaderProps {
4 | title?: string;
5 | children?: React.ReactNode;
6 | style?: StyleProp;
7 | contentStyle?: StyleProp;
8 | }
9 |
10 | export default function UntitledHeader({
11 | title,
12 | style,
13 | contentStyle,
14 | children,
15 | }: UntitledHeaderProps) {
16 | return (
17 |
18 | eader
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/app/shared-option/_layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Stack } from "expo-router";
3 |
4 | export default function Layout() {
5 | return (
6 |
12 |
13 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/app/ios-camera/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { SafeAreaView } from "react-native-safe-area-context";
3 | import IOSCameraControl from "@/components/iOSCameraControl";
4 | import { Platform } from "react-native";
5 |
6 | const isWeb = Platform.OS === "web";
7 |
8 | export default function Index() {
9 | return (
10 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useColorScheme as useRNColorScheme } from 'react-native';
3 |
4 | /**
5 | * To support static rendering, this value needs to be re-calculated on the client side for web
6 | */
7 | export function useColorScheme() {
8 | const [hasHydrated, setHasHydrated] = useState(false);
9 |
10 | useEffect(() => {
11 | setHasHydrated(true);
12 | }, []);
13 |
14 | const colorScheme = useRNColorScheme();
15 |
16 | if (hasHydrated) {
17 | return colorScheme;
18 | }
19 |
20 | return 'light';
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 | expo-env.d.ts
11 |
12 | ios/
13 | android/
14 |
15 | # Native
16 | *.orig.*
17 | *.jks
18 | *.p8
19 | *.p12
20 | *.key
21 | *.mobileprovision
22 |
23 | # Metro
24 | .metro-health-check*
25 |
26 | # debug
27 | npm-debug.*
28 | yarn-debug.*
29 | yarn-error.*
30 |
31 | # macOS
32 | .DS_Store
33 | *.pem
34 |
35 | # local env files
36 | .env*.local
37 |
38 | # typescript
39 | *.tsbuildinfo
40 |
41 | app-example
42 |
--------------------------------------------------------------------------------
/hooks/useThemeColor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about light and dark modes:
3 | * https://docs.expo.dev/guides/color-schemes/
4 | */
5 |
6 | import { Colors } from "@/constants/Colors";
7 | import { useColorScheme } from "@/hooks/useColorScheme";
8 |
9 | export function useThemeColor(
10 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark,
11 | props?: { light?: string; dark?: string }
12 | ) {
13 | const theme = useColorScheme() ?? "light";
14 | const colorFromProps = props && props[theme];
15 |
16 | if (colorFromProps) {
17 | return colorFromProps;
18 | } else {
19 | return Colors[theme][colorName];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/3d-rotate/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSharedValue } from "react-native-reanimated";
3 | import Index from "./default";
4 | import Slider from "@/components/Slider";
5 | import { View } from "react-native";
6 |
7 | export default function IosIndex() {
8 | const progress = useSharedValue(0);
9 |
10 | return (
11 |
12 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/app/slider-demo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { SafeAreaView } from "react-native-safe-area-context";
3 | import { Platform, StyleSheet } from "react-native";
4 | import PlaybackControl from "@/components/PlaybackControl";
5 |
6 | const isWeb = Platform.OS === "web";
7 |
8 | export default function SliderDemo() {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | const styles = StyleSheet.create({
17 | container: {
18 | flex: 1,
19 | justifyContent: isWeb ? "center" : "flex-end",
20 | alignItems: "center",
21 | paddingBottom: isWeb ? 0 : 72,
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/functions/index.ts:
--------------------------------------------------------------------------------
1 | import * as Haptics from "expo-haptics";
2 |
3 | export class Feedback {
4 | static light() {
5 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
6 | }
7 |
8 | static medium() {
9 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
10 | }
11 |
12 | static heavy() {
13 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
14 | }
15 |
16 | static soft() {
17 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Soft);
18 | }
19 |
20 | static success() {
21 | Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
22 | }
23 |
24 | static selection() {
25 | Haptics.selectionAsync();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/constants/Theme.ts:
--------------------------------------------------------------------------------
1 | import { fonts } from "./../node_modules/@react-navigation/native/src/theming/fonts";
2 | import { Theme } from "@react-navigation/native";
3 | import { Colors } from "./Colors";
4 |
5 | export const DarkTheme: Theme = {
6 | dark: true,
7 | colors: {
8 | primary: "rgb(10, 132, 255)",
9 | border: "rgb(39, 39, 41)",
10 | notification: "rgb(255, 69, 58)",
11 | ...Colors.dark,
12 | },
13 | fonts,
14 | };
15 |
16 | export const LightTheme: Theme = {
17 | dark: false,
18 | colors: {
19 | primary: "rgb(0, 122, 255)",
20 | border: "rgb(216, 216, 216)",
21 | notification: "rgb(255, 59, 48)",
22 | ...Colors.light,
23 | },
24 | fonts,
25 | };
26 |
--------------------------------------------------------------------------------
/app/untitled/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { useThemeColor } from "@/hooks/useThemeColor";
2 | import { Stack } from "expo-router";
3 |
4 | export default function Layout() {
5 | const bg = useThemeColor("untitledBg");
6 | return (
7 |
12 |
16 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/ui/dots.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from "react-native";
2 | import React from "react";
3 | import Svg, { Circle, Defs, Pattern, Rect } from "react-native-svg";
4 |
5 | type DotsProps = {
6 | spacing?: number;
7 | dotSize?: number;
8 | color?: string;
9 | };
10 |
11 | export default function Dots({
12 | spacing = 20,
13 | dotSize = 2,
14 | color = "#999",
15 | }: DotsProps) {
16 | return (
17 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/components/untitled/screen.ios.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import UntitledHeader, { UntitledHeaderProps } from "./header";
3 | import Transition, {
4 | useScreenAnimation,
5 | } from "react-native-screen-transitions";
6 | import UntitledBottomBar, { UntitledBottomBarProps } from "./bottom-bar";
7 |
8 | const ScrollView = Transition.ScrollView;
9 |
10 | export default function UntitledScreen({
11 | children,
12 | headerProps,
13 | barProps,
14 | }: {
15 | children?: React.ReactNode;
16 | headerProps?: UntitledHeaderProps;
17 | barProps?: UntitledBottomBarProps;
18 | }) {
19 | const props = useScreenAnimation();
20 |
21 | return (
22 | <>
23 |
24 | {children}
25 |
26 | >
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/app/slack-liquid-glass/_layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Stack } from "expo-router";
3 | import HeaderTitle, { HeaderRight } from "@/components/slack/header";
4 |
5 | export default function Layout() {
6 | return (
7 |
12 |
20 | ,
24 | headerRight: () => ,
25 | headerBackTitle: "Go Back",
26 | headerTransparent: true,
27 | }}
28 | />
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/re-captcha/index.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet } from "react-native";
2 | import React from "react";
3 | import CaptchaCard from "@/components/re-captcha/card";
4 | import { ThemedView } from "@/components/ThemedView";
5 | import { useSharedValue } from "react-native-reanimated";
6 | import Tray from "@/components/re-captcha/tray";
7 |
8 | export default function ReCaptcha() {
9 | const shrinkProgress = useSharedValue(0);
10 | const isVerified = useSharedValue(false);
11 |
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | const styles = StyleSheet.create({
21 | container: {
22 | flex: 1,
23 | alignItems: "center",
24 | justifyContent: "center",
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/components/ui/animated-text.tsx:
--------------------------------------------------------------------------------
1 | import { TextInput } from "react-native";
2 | import Animated, {
3 | SharedValue,
4 | useAnimatedProps,
5 | } from "react-native-reanimated";
6 |
7 | import type { TextInputProps } from "react-native";
8 |
9 | interface TextProps extends Omit {
10 | text: SharedValue | SharedValue;
11 | }
12 |
13 | const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
14 |
15 | export const AnimatedText = (props: TextProps) => {
16 | const { text, ...rest } = props;
17 |
18 | const animatedProps = useAnimatedProps(() => {
19 | return {
20 | text: String(text.value),
21 | } as any;
22 | });
23 |
24 | return (
25 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/components/slack/config.ts:
--------------------------------------------------------------------------------
1 | import { withSpring } from "react-native-reanimated";
2 |
3 | const PADDING = 8;
4 | const FULL_HEIGHT = 350;
5 | const CLOSED_HEIGHT = 45;
6 | const ANIMATION_DELAY = 280;
7 | const OPENED_PAD = 4;
8 |
9 | const DATA = {
10 | name: "Solarin Johnson",
11 | tabs: 3,
12 | bannerText:
13 | "This is your space. Draft messages, list your to-dos, or keep links and files handy. You can also talk to yourself here, but please bear in mind you'll have to supply both sides of the conversation.",
14 | };
15 |
16 | const SPRING_CONFIG = {
17 | stiffness: 300,
18 | damping: 30,
19 | mass: 1,
20 | overshootClamping: true,
21 | };
22 |
23 | const applySpring = (value: number) => {
24 | "worklet";
25 | return withSpring(value, SPRING_CONFIG);
26 | };
27 |
28 | export {
29 | PADDING,
30 | FULL_HEIGHT,
31 | CLOSED_HEIGHT,
32 | ANIMATION_DELAY,
33 | DATA,
34 | SPRING_CONFIG,
35 | OPENED_PAD,
36 | applySpring,
37 | };
38 |
--------------------------------------------------------------------------------
/components/Container.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollView, View } from "react-native";
2 | import { useSafeAreaInsets } from "react-native-safe-area-context";
3 | import { StyleSheet } from "react-native";
4 |
5 | const Container = ({
6 | children,
7 | noScroll,
8 | }: {
9 | children: React.ReactNode;
10 | noScroll?: boolean;
11 | }) => {
12 | const { top, bottom } = useSafeAreaInsets();
13 | const Wrapper = noScroll ? View : ScrollView;
14 | const style = { paddingTop: top, paddingBottom: bottom };
15 |
16 | const WrapperProps = noScroll
17 | ? {}
18 | : {
19 | contentContainerStyle: style,
20 | };
21 | return (
22 |
27 | {children}
28 |
29 | );
30 | };
31 |
32 | const styles = StyleSheet.create({
33 | container: {
34 | flex: 1,
35 | },
36 | });
37 |
38 | export default Container;
39 |
--------------------------------------------------------------------------------
/components/stacked-input/switch.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | View,
3 | Text,
4 | Switch as RNSwitch,
5 | SwitchProps,
6 | StyleSheet,
7 | } from "react-native";
8 | import React, { useState } from "react";
9 | import { ThemedText } from "../ThemedText";
10 |
11 | interface CustomSwitchProps extends SwitchProps {
12 | title?: string;
13 | textProps?: React.ComponentProps;
14 | }
15 |
16 | export default function Switch({
17 | title,
18 | textProps,
19 | ...switchProps
20 | }: CustomSwitchProps) {
21 |
22 | return (
23 |
24 | {title}
25 |
26 |
29 |
30 |
31 | );
32 | }
33 |
34 | const styles = StyleSheet.create({
35 | container: {
36 | flexDirection: "row",
37 | alignItems: "center",
38 | justifyContent: "space-between",
39 | flex: 1,
40 | paddingHorizontal: 12,
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/components/ResponsiveText.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from "react";
2 | import { Text, View, TextProps, Platform } from "react-native";
3 | import { ThemedText, ThemedTextProps } from "./ThemedText";
4 |
5 | type ResponsiveTextProps = {
6 | text: string;
7 | baseSize?: number;
8 | } & TextProps &
9 | ThemedTextProps;
10 |
11 | const ResponsiveText: React.FC = ({
12 | text,
13 | baseSize = 16,
14 | style,
15 | ...props
16 | }) => {
17 | const [width, setWidth] = useState(0);
18 | const fontSize = Math.max(
19 | 12,
20 | Math.min(32, baseSize * (width / 300) - text.length * 0.1)
21 | );
22 |
23 | return (
24 | setWidth(e.nativeEvent.layout.width)}
26 | style={{ width: "100%" }}
27 | >
28 |
32 | {text}
33 |
34 |
35 | );
36 | };
37 |
38 | export default ResponsiveText;
39 |
--------------------------------------------------------------------------------
/components/ui/TextLink.tsx:
--------------------------------------------------------------------------------
1 | import { TouchableOpacity, TouchableOpacityProps } from "react-native";
2 | import { ThemedText, ThemedTextProps, ThemedTextWrapper } from "../ThemedText";
3 | import * as WebBrowser from "expo-web-browser";
4 | import { isValidElement } from "react";
5 |
6 | interface TextLinkProps extends TouchableOpacityProps {
7 | children: React.ReactNode;
8 | link: string;
9 | textProps?: Omit;
10 | }
11 |
12 | const TextLink = ({
13 | children,
14 | link,
15 | textProps,
16 | ...touchableProps
17 | }: TextLinkProps) => {
18 | const onPress = () => {
19 | WebBrowser.openBrowserAsync(link);
20 | };
21 |
22 | const content =
23 | typeof children === "string" ? (
24 | {children}
25 | ) : isValidElement(children) ? (
26 |
27 | {children}
28 |
29 | ) : null;
30 |
31 | return (
32 |
33 | {content}
34 |
35 | );
36 | };
37 |
38 | export default TextLink;
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Playground 🎮
2 |
3 | My personal [Expo](https://expo.dev) playground for React Native animations and UI experiments.
4 |
5 | ## Prerequisites
6 |
7 | This project requires a development client - you cannot use Expo Go.
8 |
9 | ## Get Started
10 |
11 | ### 1. Install dependencies
12 |
13 | ```bash
14 | npm install
15 | ```
16 |
17 | ### 2. Build the development client
18 |
19 | **For iOS Simulator:**
20 |
21 | ```bash
22 | npx expo run:ios
23 | ```
24 |
25 | **For Android Emulator:**
26 |
27 | ```bash
28 | npx expo run:android
29 | ```
30 |
31 | ### 3. Start the development server
32 |
33 | ```bash
34 | npx expo start --dev-client
35 | ```
36 |
37 | ## What's Inside 🧪
38 |
39 | - **Keyboard Controller** - Keyboard handling
40 | - **Gesture Navigation** - Custom gesture experiments
41 | - **Twitter Profile** - Animated profile interactions
42 | - **Threads Pull Refresh** - Pull-to-refresh animations
43 | - **Grok Sidebar** - Animated sidebar with blur effects
44 |
45 | ## Notes
46 |
47 | - ⚠️ Expo Go is NOT supported
48 | - 📱 Development client must be installed first
49 | - 🔄 Rebuild only when native dependencies change
50 |
--------------------------------------------------------------------------------
/components/safari-bar/article.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { View, StyleSheet } from "react-native";
3 | import { ThemedText } from "../ThemedText";
4 |
5 | type ArticleProps = {
6 | title: string;
7 | content: string[];
8 | index: number;
9 | };
10 |
11 | const Article: React.FC = ({ title, content }) => {
12 | return (
13 |
14 |
15 |
16 | {title}
17 |
18 |
19 |
20 | {content.map((paragraph, i) => (
21 |
22 | {paragraph}
23 |
24 | ))}
25 |
26 |
27 | );
28 | };
29 |
30 | const styles = StyleSheet.create({
31 | article: {
32 | marginVertical: 10,
33 | paddingHorizontal: 16,
34 | padding: 10,
35 | borderRadius: 5,
36 | },
37 | section: {
38 | marginTop: 10,
39 | gap: 20,
40 | },
41 | content: {
42 | opacity: 0.8,
43 | },
44 | });
45 |
46 | export default Article;
47 |
--------------------------------------------------------------------------------
/components/stacked-input/provider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, use } from "react";
2 | import { StyleProp, TextInputProps, ViewStyle } from "react-native";
3 | import { SharedValue } from "react-native-reanimated";
4 |
5 | type SharedVal = {
6 | currentIndex: SharedValue;
7 | itemStyles?: StyleProp;
8 | itemProps?: TextInputProps;
9 | maxIndex?: number;
10 | minIndex?: number;
11 | };
12 |
13 | const StackedInputContext = createContext(undefined);
14 |
15 | export function Provider({
16 | children,
17 | currentIndex,
18 | itemStyles,
19 | itemProps,
20 | maxIndex,
21 | minIndex,
22 | }: {
23 | children: React.ReactNode;
24 | } & SharedVal) {
25 | return (
26 |
29 | {children}
30 |
31 | );
32 | }
33 |
34 | export function useStackedInput(): SharedVal {
35 | const context = use(StackedInputContext);
36 | if (!context) {
37 | throw new Error(
38 | "useStackedInput must be used within a StackedInputProvider"
39 | );
40 | }
41 | return context;
42 | }
43 |
--------------------------------------------------------------------------------
/app/untitled/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import UntitledScreen from "@/components/untitled/screen";
3 | import Ionicons from "@expo/vector-icons/Ionicons";
4 | import UntitledButton, {
5 | UntitledButtonWrapper,
6 | } from "@/components/untitled/button";
7 | import {
8 | UntitledCardMini,
9 | UntitledCardMiniWrapper,
10 | } from "@/components/untitled/card";
11 |
12 | export default function Index() {
13 | return (
14 | }}>
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | const Header = () => {
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/app/slack-liquid-glass/chat.tsx:
--------------------------------------------------------------------------------
1 | import SlackBanner from "@/components/slack/banner";
2 | import { CLOSED_HEIGHT } from "@/components/slack/config";
3 | import MessageBar from "@/components/slack/message-bar";
4 | import MessageBox from "@/components/slack/message-box";
5 | import { ThemedView } from "@/components/ThemedView";
6 | import { StyleSheet, ScrollView, View } from "react-native";
7 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
8 |
9 | const MSG_BOX_HEIGHT = 60;
10 |
11 | export default function Index() {
12 | return (
13 |
14 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | const styles = StyleSheet.create({
30 | container: {
31 | flex: 1,
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/components/slack/banner.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet } from "react-native";
2 | import React from "react";
3 | import { Image } from "expo-image";
4 | import { ThemedText } from "../ThemedText";
5 | import { DATA } from "./config";
6 |
7 | export default function SlackBanner() {
8 | return (
9 |
10 |
14 |
15 | {DATA.name}
16 |
17 |
18 | {DATA.bannerText}
19 |
20 |
21 | );
22 | }
23 |
24 | const styles = StyleSheet.create({
25 | container: {
26 | padding: 16,
27 | // backgroundColor: "lightgray",
28 | },
29 | image: {
30 | width: 54,
31 | aspectRatio: 1,
32 | borderRadius: 12,
33 | borderCurve: "continuous",
34 | },
35 | title: {
36 | fontSize: 21,
37 | marginVertical: 12,
38 | fontWeight: "700",
39 | letterSpacing: -0.2,
40 | },
41 | sub: {
42 | fontSize: 17,
43 | marginVertical: 8,
44 | lineHeight: 23,
45 | opacity: 0.8,
46 | },
47 | });
48 |
--------------------------------------------------------------------------------
/components/untitled/button.tsx:
--------------------------------------------------------------------------------
1 | import { View, Pressable, StyleSheet } from "react-native";
2 | import React, { ReactElement } from "react";
3 | import { ThemedTextWrapper } from "../ThemedText";
4 | import { useThemeColor } from "@/hooks/useThemeColor";
5 | import PressableBounce from "../PresableBounce";
6 |
7 | export default function UntitledButton({
8 | children,
9 | onPress,
10 | }: {
11 | children?: ReactElement;
12 | onPress?: () => void;
13 | }) {
14 | const text = useThemeColor("text");
15 | return (
16 |
20 | {children && {children}}
21 |
22 | );
23 | }
24 |
25 | export const UntitledButtonWrapper = ({
26 | children,
27 | }: {
28 | children?: React.ReactNode;
29 | }) => {
30 | return {children};
31 | };
32 |
33 | const styles = StyleSheet.create({
34 | button: {
35 | borderRadius: 16,
36 | width: 44,
37 | height: 44,
38 | alignItems: "center",
39 | justifyContent: "center",
40 | borderCurve: "continuous",
41 | },
42 | wrapper: {
43 | flexDirection: "row",
44 | gap: 10,
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/app/slack-liquid-glass/index.ios.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedTextWrapper } from "@/components/ThemedText";
2 | import { ThemedView } from "@/components/ThemedView";
3 | import { GlassView } from "expo-glass-effect";
4 | import { Link } from "expo-router";
5 | import { StyleSheet } from "react-native";
6 | import { SafeAreaView } from "react-native-safe-area-context";
7 |
8 | export default function Index() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | Open Chat
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | const styles = StyleSheet.create({
25 | container: {
26 | flex: 1,
27 | justifyContent: "center",
28 | alignItems: "center",
29 | },
30 | text: {
31 | padding: 16,
32 | textAlign: "center",
33 | width: 280,
34 | fontSize: 16,
35 | },
36 | glass: {
37 | borderRadius: 40,
38 | alignItems: "center",
39 | justifyContent: "center",
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/components/ui/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Pressable, StyleSheet, ViewStyle, TextStyle } from "react-native";
3 | import { ThemedText, ThemedTextProps } from "../ThemedText";
4 |
5 | interface ButtonProps {
6 | title: string;
7 | onPress?: () => void;
8 | style?: ViewStyle;
9 | textStyle?: TextStyle;
10 | bgcolor?: string;
11 | color?: string;
12 | textProps?: ThemedTextProps;
13 | }
14 |
15 | const Button: React.FC = ({
16 | title,
17 | onPress,
18 | style,
19 | textStyle,
20 | bgcolor = "#007bff",
21 | color = "#fff",
22 | textProps,
23 | }) => {
24 | return (
25 |
29 |
34 | {title}
35 |
36 |
37 | );
38 | };
39 |
40 | const styles = StyleSheet.create({
41 | button: {
42 | padding: 10,
43 | alignItems: "center",
44 | justifyContent: "center",
45 | backgroundColor: "#007bff",
46 | borderRadius: 50,
47 | borderCurve: "continuous",
48 | },
49 | text: {
50 | color: "#fff",
51 | fontSize: 16,
52 | },
53 | });
54 |
55 | export default Button;
56 |
--------------------------------------------------------------------------------
/components/stacked-input/trigger.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | ReactElement,
3 | cloneElement,
4 | ComponentProps,
5 | isValidElement,
6 | } from "react";
7 | import { Pressable, PressableProps } from "react-native";
8 | import { useStackedInput } from "./provider";
9 |
10 | interface TriggerProps extends Omit {
11 | type: "next" | "previous";
12 | asChild?: boolean;
13 | children: ReactElement>;
14 | }
15 |
16 | export default function Trigger({
17 | type,
18 | asChild,
19 | children,
20 | ...props
21 | }: TriggerProps) {
22 | const { currentIndex, minIndex = 0, maxIndex = 1 } = useStackedInput();
23 |
24 | const handlePress = () => {
25 | if (type === "next") {
26 | if (currentIndex.value < maxIndex) {
27 | currentIndex.value += 1;
28 | }
29 | } else {
30 | if (currentIndex.value > minIndex) {
31 | currentIndex.value -= 1;
32 | }
33 | }
34 | };
35 |
36 | if (asChild && isValidElement(children)) {
37 | return cloneElement(children, {
38 | onPress: () => {
39 | (children.props as { onPress?: () => void }).onPress?.();
40 | handlePress();
41 | },
42 | });
43 | }
44 |
45 | return (
46 |
47 | {children}
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/app/shared-option/expanded.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Pressable } from "react-native";
2 | import React from "react";
3 | import Animated from "react-native-reanimated";
4 | import { router } from "expo-router";
5 | import { SHARED_DATA } from "@/constants";
6 | import { Image } from "expo-image";
7 |
8 | const AnimatedImage = Animated.createAnimatedComponent(Image);
9 |
10 | export default function Index() {
11 | const colors = ["red", "blue", "green", "yellow"];
12 | const IMAGES = SHARED_DATA.apps.map((app) => app.logo);
13 |
14 | return (
15 | {
18 | router.back();
19 | }}
20 | >
21 | {IMAGES.map((image, index) => (
22 |
30 | ))}
31 |
32 | );
33 | }
34 |
35 | const styles = StyleSheet.create({
36 | container: {
37 | flex: 1,
38 | flexDirection: "column",
39 | justifyContent: "center",
40 | gap: 24,
41 | paddingHorizontal: 24,
42 | },
43 | box: {
44 | width: 80,
45 | height: 80,
46 | borderRadius: 40,
47 | overflow: "hidden",
48 | },
49 | });
50 |
--------------------------------------------------------------------------------
/app/untitled/[id].tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { router } from "expo-router";
3 | import UntitledScreen from "@/components/untitled/screen";
4 | import Ionicons from "@expo/vector-icons/Ionicons";
5 | import UntitledButton, {
6 | UntitledButtonWrapper,
7 | } from "@/components/untitled/button";
8 | import { UntitledCardLarge } from "@/components/untitled/card";
9 |
10 | export default function Index() {
11 | return (
12 | }}
14 | barProps={{ type: "fill" }}
15 | >
16 |
17 |
18 | );
19 | }
20 |
21 | const Header = () => {
22 | const goBack = () => {
23 | router.back();
24 | };
25 | return (
26 | <>
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | >
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/components/ProgressiveFade.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet, ViewStyle } from "react-native";
2 | import { useThemeColor } from "@/hooks/useThemeColor";
3 | import { useSafeAreaInsets } from "react-native-safe-area-context";
4 | import Animated from "react-native-reanimated";
5 |
6 | type ProgressiveFadeProps = {
7 | direction?: "top" | "bottom";
8 | height?: number;
9 | style?: ViewStyle | ViewStyle[];
10 | };
11 |
12 | export default function ProgressiveFade({
13 | direction = "top",
14 | height = 64,
15 | style: customStyle,
16 | }: ProgressiveFadeProps) {
17 | const color = useThemeColor("background");
18 | const insets = useSafeAreaInsets();
19 |
20 | return (
21 |
31 |
41 |
42 | );
43 | }
44 |
45 | const styles = StyleSheet.create({
46 | container: {
47 | width: "100%",
48 | position: "absolute",
49 | left: 0,
50 | right: 0,
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/components/ThemedView.tsx:
--------------------------------------------------------------------------------
1 | import { View, type ViewProps } from "react-native";
2 |
3 | import { useThemeColor } from "@/hooks/useThemeColor";
4 | import { Colors } from "@/constants/Colors";
5 | import { cloneElement } from "react";
6 |
7 | export type ThemedViewProps = ViewProps & {
8 | lightColor?: string;
9 | darkColor?: string;
10 | invert?: boolean;
11 | colorName?: keyof typeof Colors.light & keyof typeof Colors.dark;
12 | };
13 |
14 | export function ThemedView({
15 | style,
16 | lightColor,
17 | darkColor,
18 | invert,
19 | colorName = "background",
20 | ...otherProps
21 | }: ThemedViewProps) {
22 | const backgroundColor = useThemeColor(invert ? "text" : colorName, {
23 | light: lightColor,
24 | dark: darkColor,
25 | });
26 |
27 | return ;
28 | }
29 |
30 | export function ThemedViewWrapper({
31 | children,
32 | lightColor,
33 | darkColor,
34 | invert = false,
35 | colorName = "background",
36 | style,
37 | ...rest
38 | }: ThemedViewProps & { children: React.ReactElement }) {
39 | const backgroundColor = useThemeColor(invert ? "text" : colorName, {
40 | light: lightColor,
41 | dark: darkColor,
42 | });
43 |
44 | const combinedStyle = [{ backgroundColor }, style];
45 |
46 | return cloneElement(children, {
47 | style: [(children.props as any).style ?? {}, ...combinedStyle],
48 | ...rest,
49 | });
50 | }
51 |
--------------------------------------------------------------------------------
/components/safari-bar/header.tsx:
--------------------------------------------------------------------------------
1 | import { View, TouchableOpacity, StyleSheet } from "react-native";
2 | import { Ionicons } from "@expo/vector-icons";
3 | import { ThemedText, ThemedTextWrapper } from "../ThemedText";
4 | import { useThemeColor } from "@/hooks/useThemeColor";
5 | import ResponsiveText from "../ResponsiveText";
6 |
7 | interface HeaderProps {
8 | title: string;
9 | content: string;
10 | onBackPress: () => void;
11 | }
12 |
13 | const Header = ({ title, content, onBackPress }: HeaderProps) => {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 | {content}
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | const styles = StyleSheet.create({
29 | container: {
30 | flexDirection: "column",
31 | alignItems: "center",
32 | paddingHorizontal: 16,
33 | paddingVertical: 12,
34 | gap: 4,
35 | },
36 | nav: {
37 | flexDirection: "row",
38 | width: "100%",
39 | paddingVertical: 16,
40 | alignItems: "center",
41 | justifyContent: "space-between",
42 | },
43 | head: {
44 | flexDirection: "column",
45 | width: "100%",
46 | gap: 6,
47 | },
48 | content: {
49 | paddingVertical: 5,
50 | },
51 | title: {
52 | fontSize: 28,
53 | },
54 | });
55 |
56 | export default Header;
57 |
--------------------------------------------------------------------------------
/app/threads-pull-refresh/index.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 | import React from "react";
3 | import RefreshLogo from "@/components/RefreshLogo";
4 | import Animated from "react-native-reanimated";
5 | import {
6 | useSharedValue,
7 | useAnimatedScrollHandler,
8 | } from "react-native-reanimated";
9 | import { useSafeAreaInsets } from "react-native-safe-area-context";
10 |
11 | const path =
12 | "M19.25 8.50488C17.6729 2.63804 12.25 3.00452 12.25 3.00452C12.25 3.00452 4.75 2.50512 4.75 12C4.75 21.4949 12.25 20.9955 12.25 20.9955C12.25 20.9955 16.7077 21.2924 18.75 17.0782C19.4167 15.2204 19.25 11.5049 12.75 11.5049C12.75 11.5049 9.75 11.5049 9.75 14.0049C9.75 14.9812 10.75 16.0049 12.25 16.0049C13.75 16.0049 15.4212 14.9777 15.75 13.0049C16.75 7.00488 11.25 6.50488 9.75 9.00488";
13 |
14 | export default function ThreadPullRefresh() {
15 | const scrollY = useSharedValue(0);
16 | const { top } = useSafeAreaInsets();
17 |
18 | const scrollHandler = useAnimatedScrollHandler({
19 | onScroll: (event) => {
20 | scrollY.value = event.contentOffset.y;
21 | },
22 | });
23 |
24 | return (
25 |
30 |
31 |
32 | );
33 | }
34 |
35 | const styles = StyleSheet.create({
36 | container: {
37 | flex: 1,
38 | padding: 16,
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "[my-app]",
4 | "slug": "my-app",
5 | "owner": "solarin",
6 | "version": "1.0.0",
7 | "orientation": "portrait",
8 | "icon": "./assets/images/icon.png",
9 | "scheme": "myapp",
10 | "userInterfaceStyle": "automatic",
11 | "newArchEnabled": true,
12 | "ios": {
13 | "supportsTablet": true,
14 | "bundleIdentifier": "com.myapp.solarin"
15 | },
16 | "android": {
17 | "adaptiveIcon": {
18 | "foregroundImage": "./assets/images/adaptive-icon.png",
19 | "backgroundColor": "#181818"
20 | },
21 | "edgeToEdgeEnabled": true,
22 | "package": "com.myapp.solarin"
23 | },
24 | "web": {
25 | "bundler": "metro",
26 | "output": "static",
27 | "favicon": "./assets/images/dp.png"
28 | },
29 | "plugins": [
30 | "expo-router",
31 | [
32 | "expo-splash-screen",
33 | {
34 | "image": "./assets/images/splash-icon-light.png",
35 | "imageWidth": 250,
36 | "backgroundColor": "#F5F5F7",
37 | "dark": {
38 | "image": "./assets/images/splash-icon-dark.png",
39 | "backgroundColor": "#181818"
40 | }
41 | }
42 | ],
43 | "expo-web-browser"
44 | ],
45 | "experiments": {
46 | "typedRoutes": true
47 | },
48 | "extra": {
49 | "router": {
50 | "origin": false
51 | },
52 | "eas": {
53 | "projectId": "9efc0bb6-df7f-44a9-9544-9e52e73553bb"
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/components/PresableBounce.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { GestureResponderEvent, Pressable, PressableProps } from "react-native";
3 | import Animated, {
4 | Easing,
5 | useAnimatedStyle,
6 | useSharedValue,
7 | withSpring,
8 | withTiming,
9 | } from "react-native-reanimated";
10 |
11 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
12 | const easing = Easing.inOut(Easing.ease);
13 |
14 | interface PressableBounceProps extends PressableProps {
15 | children: React.ReactNode;
16 | bounceScale?: number;
17 | duration?: number;
18 | }
19 |
20 | const PressableBounce: React.FC = ({
21 | children,
22 | bounceScale = 0.95,
23 | duration = 150,
24 | ...props
25 | }) => {
26 | const scale = useSharedValue(1);
27 |
28 | const animatedStyle = useAnimatedStyle(() => {
29 | return {
30 | transform: [{ scale: scale.value }],
31 | };
32 | });
33 |
34 | const handlePressIn = (event: GestureResponderEvent) => {
35 | scale.value = withTiming(bounceScale, {
36 | duration,
37 | easing,
38 | });
39 | props.onPressIn && props.onPressIn(event);
40 | };
41 |
42 | const handlePressOut = (event: GestureResponderEvent) => {
43 | scale.value = withSpring(1);
44 | props.onPressOut && props.onPressOut(event);
45 | };
46 |
47 | return (
48 |
54 | {children}
55 |
56 | );
57 | };
58 |
59 | export default PressableBounce;
60 |
--------------------------------------------------------------------------------
/components/HeadComponent.tsx:
--------------------------------------------------------------------------------
1 | import Head from "expo-router/head";
2 | import { Platform } from "react-native";
3 |
4 | const isWeb = Platform.OS === "web";
5 |
6 | export default function HeadComponent() {
7 | if (!isWeb) return null;
8 |
9 | return (
10 |
11 | My App
12 |
16 |
17 |
18 |
19 | {/* Open Graph / Facebook */}
20 |
24 |
25 |
26 |
27 |
31 |
32 |
33 | {/* Twitter */}
34 |
38 |
39 |
40 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/components/ui/glassy-view.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, View } from "react-native";
2 | import { GlassContainer, GlassView } from "expo-glass-effect";
3 | import { Image } from "expo-image";
4 | import { ThemedText } from "../ThemedText";
5 |
6 | export default function GlassViewComponent() {
7 | return (
8 |
9 |
15 |
16 |
22 |
27 |
32 |
33 |
34 | );
35 | }
36 |
37 | const styles = StyleSheet.create({
38 | container: {
39 | flex: 1,
40 | },
41 | backgroundImage: {
42 | ...StyleSheet.absoluteFillObject,
43 | width: "100%",
44 | height: "100%",
45 | },
46 | containerStyle: {
47 | position: "absolute",
48 | top: 200,
49 | left: 50,
50 | width: 250,
51 | height: 100,
52 | flexDirection: "row",
53 | alignItems: "center",
54 | gap: 10,
55 | },
56 | glass1: {
57 | width: 60,
58 | height: 60,
59 | borderRadius: 30,
60 | },
61 | glass2: {
62 | width: 50,
63 | height: 50,
64 | borderRadius: 25,
65 | },
66 | glass3: {
67 | width: 40,
68 | height: 40,
69 | borderRadius: 20,
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4 | */
5 |
6 | const tintColorLight = "#0a7ea4";
7 | const tintColorDark = "#fff";
8 |
9 | const globalColors = {
10 | systemBlue: "#2D6AF6",
11 | appleRed: "#FF3B30",
12 | };
13 |
14 | export const Colors = {
15 | light: {
16 | waBg: "#F4F4F4",
17 | text: "#181818",
18 | background: "#F5F5F7",
19 | barColor: "#FFFFFF",
20 | waCard: "#FFFFFF",
21 | card: "#F5F5F7",
22 | tint: tintColorLight,
23 | untitledBg: "#FFFFFF",
24 | untitledGradient1: "#D55FDC",
25 | untitledGradient2: "#F5C1DB",
26 | untitledBarBg: "#222222",
27 | slackBg: "#FFFFFF",
28 | slackText: "#1C1C1C",
29 | safariBg: "#F2F2F6",
30 | captchaBg: "#FFFFFF",
31 | captchaCardBg: "#F7F7F7",
32 | captchaCheckboxBg: "#272727",
33 | captchaCheckboxInitialBg: "#FFFFFF",
34 | popUpCardBg: "#FFFFFF",
35 | cardBg3D: "#E2E2E8",
36 | ...globalColors,
37 | },
38 | dark: {
39 | waBg: "#000000",
40 | text: "#F5F5F7",
41 | background: "#121212",
42 | card: "#121212",
43 | barColor: "#1A1A1A",
44 | waCard: "#171718",
45 | tint: tintColorDark,
46 | untitledBg: "#191918",
47 | untitledGradient1: "#D55FDC",
48 | untitledGradient2: "#F5C1DB",
49 | untitledBarBg: "#323232",
50 | slackBg: "#1B1D21",
51 | slackText: "#D2D2D4",
52 | safariBg: "#1C1C1E",
53 | captchaBg: "#0E0E0E",
54 | captchaCardBg: "#181818",
55 | captchaCheckboxBg: "#343434",
56 | captchaCheckboxInitialBg: "#656565",
57 | popUpCardBg: "#1A1A1A",
58 | cardBg3D: "#252528",
59 | ...globalColors,
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/app/gesture-menu/index.tsx:
--------------------------------------------------------------------------------
1 | import GestureMenu, { GestureMenuItem } from "@/components/GestureMenu";
2 | import { Feedback } from "@/functions";
3 | import { Lock, Search } from "lucide-react-native";
4 | import { View, Text, Alert } from "react-native";
5 | import { SafeAreaView } from "react-native-safe-area-context";
6 |
7 | export default function GestureMenuScreen() {
8 | const onPress = (label: string) => {
9 | // Alert.alert(`Pressed ${label}`);
10 | };
11 | return (
12 |
15 | {(() => {
16 | const items = [
17 | { label: "Option 1", icon: },
18 | { label: "Option 2" },
19 | { label: "Option 3" },
20 | // { label: "Option 4" },
21 | // { label: "Option 5" },
22 | ];
23 |
24 | return (
25 | ,
43 | }}
44 | roundedIndicator={false}
45 | >
46 | {items.map(({ label, icon }) => (
47 | onPress(label)}
51 | icon={icon}
52 | />
53 | ))}
54 |
55 | );
56 | })()}
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/app/wa-status/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText } from "@/components/ThemedText";
2 | import { Stack, useRouter } from "expo-router";
3 | import { BlurView } from "expo-blur";
4 | import { Pressable, StyleSheet } from "react-native";
5 | import { useThemeColor } from "@/hooks/useThemeColor";
6 |
7 | export default function Layout() {
8 | const bg = useThemeColor("waBg");
9 |
10 | return (
11 | ,
16 | headerBackground: () => (
17 |
18 | ),
19 | headerTransparent: true,
20 | contentStyle: { backgroundColor: bg },
21 | }}
22 | >
23 |
29 |
35 | ,
41 | headerBackground: () => (
42 |
43 | ),
44 | headerTransparent: true,
45 | contentStyle: { backgroundColor: bg },
46 | }}
47 | />
48 |
49 | );
50 | }
51 | const HeaderRight = ({
52 | text = "Edit",
53 | width = 50,
54 | }: {
55 | text?: string;
56 | width?: number;
57 | }) => {
58 | const router = useRouter();
59 |
60 | return (
61 |
70 | {text}
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const SHARED_DATA = {
2 | text: "I've found the top 5 music apps that match your preferences best.",
3 | apps: [
4 | {
5 | name: "Spotify",
6 | logo: "https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png",
7 | description:
8 | "Listen to millions of songs and podcasts with tailored recommendations and custom playlists for a personalized music experience.",
9 | platforms: ["iOS", "Android"],
10 | },
11 | {
12 | name: "Apple Music",
13 | logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5f/Apple_Music_icon.svg/120px-Apple_Music_icon.svg.png",
14 | description:
15 | "Access over 100 million songs with spatial audio, lossless quality, and exclusive releases from your favorite artists worldwide.",
16 | platforms: ["iOS", "Android", "Web"],
17 | },
18 | {
19 | name: "YouTube Music",
20 | logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Youtube_Music_icon.svg/120px-Youtube_Music_icon.svg.png",
21 | description:
22 | "Discover music videos, live performances, official tracks, and remixes from your favorite artists all in one streaming platform.",
23 | platforms: ["iOS", "Android", "Web"],
24 | },
25 | {
26 | name: "Tidal",
27 | logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTtk-KVZgKJ30k8DDSqdxmMGDy6z11515g-oA&s",
28 | description:
29 | "Experience high-fidelity sound quality with lossless audio, exclusive content, and direct artist support through this premium streaming service.",
30 | platforms: ["iOS", "Android"],
31 | },
32 | {
33 | name: "SoundCloud",
34 | logo: "https://icon-icons.com/download-file?file=https%3A%2F%2Fimages.icon-icons.com%2F2972%2FPNG%2F512%2Fsoundcloud_logo_icon_186893.png&id=186893&pack_or_individual=pack",
35 | description:
36 | "Explore indie tracks, emerging artists, remixes, and underground music from around the world in this creator-focused platform.",
37 | platforms: ["iOS", "Android", "Web"],
38 | },
39 | ],
40 | };
41 |
--------------------------------------------------------------------------------
/components/untitled/header.ios.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet, StyleProp, ViewStyle } from "react-native";
2 | import React from "react";
3 | import { useSafeAreaInsets } from "react-native-safe-area-context";
4 | import { ThemedText } from "../ThemedText";
5 | import Animated, {
6 | interpolate,
7 | useAnimatedStyle,
8 | } from "react-native-reanimated";
9 | import { useScreenAnimation } from "react-native-screen-transitions";
10 |
11 | export interface UntitledHeaderProps {
12 | title?: string;
13 | children?: React.ReactNode;
14 | style?: StyleProp;
15 | contentStyle?: StyleProp;
16 | }
17 |
18 | export default function UntitledHeader({
19 | title,
20 | style,
21 | contentStyle,
22 | children,
23 | }: UntitledHeaderProps) {
24 | const { top } = useSafeAreaInsets();
25 | const props = useScreenAnimation();
26 |
27 | const headerAnimatedStyle = useAnimatedStyle(() => {
28 | const {
29 | progress,
30 | layouts: {
31 | screen: { width },
32 | },
33 | } = props.value;
34 |
35 | return {
36 | opacity: interpolate(progress, [0, 1, 2], [0, 1, 0]),
37 | transform: [
38 | {
39 | translateX: interpolate(progress, [0, 1, 2], [-width, 0, width]),
40 | },
41 | ],
42 | };
43 | });
44 |
45 | return (
46 |
56 |
57 | {title && (
58 |
59 | {title}
60 |
61 | )}
62 | {children}
63 |
64 |
65 | );
66 | }
67 |
68 | const styles = StyleSheet.create({
69 | container: {
70 | padding: 16,
71 | paddingHorizontal: 20,
72 | },
73 | header: {
74 | flexDirection: "row",
75 | alignItems: "center",
76 | justifyContent: "space-between",
77 | },
78 | title: {
79 | fontSize: 23,
80 | },
81 | });
82 |
--------------------------------------------------------------------------------
/components/ThemedText.tsx:
--------------------------------------------------------------------------------
1 | import { Text, type TextProps, StyleSheet } from "react-native";
2 | import { useThemeColor } from "@/hooks/useThemeColor";
3 | import { cloneElement, ReactElement } from "react";
4 | import { Colors } from "@/constants/Colors";
5 |
6 | export type ThemedTextProps = TextProps & {
7 | type?:
8 | | "default"
9 | | "title"
10 | | "defaultSemiBold"
11 | | "subtitle"
12 | | "link"
13 | | "bold"
14 | | "regular";
15 | colorName?: keyof typeof Colors.light & keyof typeof Colors.dark;
16 | };
17 |
18 | export function ThemedText({
19 | style,
20 | type = "default",
21 | colorName = "text",
22 | ...rest
23 | }: ThemedTextProps) {
24 | const color = useThemeColor(colorName);
25 | const variantKey = type as keyof typeof styles;
26 |
27 | return (
28 |
32 | );
33 | }
34 |
35 | export function ThemedTextWrapper({
36 | children,
37 | type = "default",
38 | colorName = "text",
39 | style,
40 | ignoreStyle = true,
41 | ...rest
42 | }: ThemedTextProps & { children: ReactElement; ignoreStyle?: boolean }) {
43 | const color = useThemeColor(colorName);
44 | const variantKey = type as keyof typeof styles;
45 |
46 | const combinedStyle = [
47 | { color, fontFamily: "InterMedium" },
48 | !ignoreStyle && styles[variantKey],
49 | style,
50 | ];
51 |
52 | return cloneElement(children, {
53 | style: [(children.props as any).style ?? {}, ...combinedStyle],
54 | color: color,
55 | ...rest,
56 | });
57 | }
58 |
59 | const styles = StyleSheet.create({
60 | default: {
61 | fontSize: 16,
62 | },
63 | defaultSemiBold: {
64 | fontSize: 17,
65 | fontFamily: "InterSemiBold",
66 | },
67 | title: {
68 | fontFamily: "InterSemiBold",
69 | fontSize: 20,
70 | },
71 | subtitle: {
72 | fontSize: 15,
73 | },
74 | link: {
75 | lineHeight: 30,
76 | fontSize: 16,
77 | color: "#0a7ea4",
78 | },
79 | bold: {
80 | fontSize: 16,
81 | fontFamily: "InterBold",
82 | },
83 | regular: {
84 | fontSize: 16,
85 | fontFamily: "InterRegular",
86 | },
87 | });
88 |
--------------------------------------------------------------------------------
/app/untitled/_layout.ios.tsx:
--------------------------------------------------------------------------------
1 | import { useThemeColor } from "@/hooks/useThemeColor";
2 | import type {
3 | ParamListBase,
4 | StackNavigationState,
5 | } from "@react-navigation/native";
6 | import { withLayoutContext } from "expo-router";
7 | import { interpolate } from "react-native-reanimated";
8 | import Transition, {
9 | createNativeStackNavigator,
10 | type NativeStackNavigationEventMap,
11 | type NativeStackNavigationOptions,
12 | } from "react-native-screen-transitions";
13 |
14 | const { Navigator } = createNativeStackNavigator();
15 |
16 | export const Stack = withLayoutContext<
17 | NativeStackNavigationOptions,
18 | typeof Navigator,
19 | StackNavigationState,
20 | NativeStackNavigationEventMap
21 | >(Navigator);
22 |
23 | const SPRING_CONFIG = {
24 | damping: 50,
25 | stiffness: 400,
26 | mass: 1,
27 | overshootClamping: true,
28 | restDisplacementThreshold: 0.0001,
29 | restSpeedThreshold: 0.0001,
30 | };
31 |
32 | // const TIMING_CONFIG = { duration: 100, easing: Easing.inOut(Easing.ease) };
33 | // const SPRING_CONFIG = Transition.specs.DefaultSpec;
34 |
35 | export default function Layout() {
36 | const bg = useThemeColor("untitledBg");
37 | return (
38 |
43 |
47 | {
59 | "worklet";
60 |
61 | const x = interpolate(progress, [0, 1, 2], [width, 0, -width]);
62 | return {
63 | contentStyle: {
64 | transform: [{ translateX: x }],
65 | },
66 | };
67 | },
68 | transitionSpec: {
69 | close: SPRING_CONFIG,
70 | open: SPRING_CONFIG,
71 | },
72 | }}
73 | />
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/components/notify/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useContext,
4 | ReactNode,
5 | useState,
6 | useEffect,
7 | } from "react";
8 | import { View, StyleSheet } from "react-native";
9 | import Banner from "./banner";
10 | import { MessageType, NotifyContextType } from "./type";
11 | import { useSharedValue } from "react-native-reanimated";
12 | import { useKeyboardHandler } from "react-native-keyboard-controller";
13 |
14 | const NotifyContext = createContext(null);
15 |
16 | let messageId = 0;
17 | const MAX_MESSAGES = 2;
18 |
19 | export const NotifyProvider = ({ children }: { children: ReactNode }) => {
20 | const [messages, setMessages] = useState([]);
21 | const messageCount = useSharedValue(0);
22 | const progress = useSharedValue(0);
23 | const height = useSharedValue(0);
24 |
25 | const notify: NotifyContextType["notify"] = (msg, options) => {
26 | messageCount.value = messageCount.value + 1;
27 | setMessages((prev) => {
28 | const newMessage = { id: messageId++, text: msg, options };
29 | let newMessages = [...prev, newMessage];
30 |
31 | if (newMessages.length > MAX_MESSAGES) {
32 | newMessages = newMessages.slice(newMessages.length - MAX_MESSAGES);
33 | }
34 |
35 | return newMessages;
36 | });
37 | };
38 |
39 | useKeyboardHandler(
40 | {
41 | onMove: (e) => {
42 | "worklet";
43 | progress.value = e.progress;
44 | height.value = e.height;
45 | },
46 | },
47 |
48 | []
49 | );
50 |
51 | return (
52 |
53 | {children}
54 |
55 | {messages.map((message, index) => (
56 |
63 | ))}
64 |
65 |
66 | );
67 | };
68 |
69 | export const useNotify = () => {
70 | const ctx = useContext(NotifyContext);
71 | if (!ctx) throw new Error("useNotify must be used inside NotifyProvider");
72 | return ctx;
73 | };
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-app",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo run:android",
8 | "ios": "expo run:ios",
9 | "web": "expo start --web",
10 | "lint": "expo lint"
11 | },
12 | "dependencies": {
13 | "@expo/metro-runtime": "~6.1.2",
14 | "@expo/ui": "~0.2.0-beta.7",
15 | "@expo/vector-icons": "^15.0.2",
16 | "@react-native-masked-view/masked-view": "^0.3.2",
17 | "@react-navigation/bottom-tabs": "^7.2.0",
18 | "@react-navigation/drawer": "^7.3.9",
19 | "@react-navigation/native": "^7.0.14",
20 | "expo": "54.0.21",
21 | "expo-blur": "~15.0.7",
22 | "expo-constants": "~18.0.8",
23 | "expo-dev-client": "~6.0.16",
24 | "expo-drawpad": "^0.2.0",
25 | "expo-font": "~14.0.8",
26 | "expo-glass-effect": "~0.1.5",
27 | "expo-haptics": "~15.0.7",
28 | "expo-image": "~3.0.10",
29 | "expo-linear-gradient": "~15.0.7",
30 | "expo-linking": "~8.0.8",
31 | "expo-router": "~6.0.14",
32 | "expo-splash-screen": "~31.0.10",
33 | "expo-status-bar": "~3.0.8",
34 | "expo-symbols": "~1.0.7",
35 | "expo-system-ui": "~6.0.8",
36 | "expo-web-browser": "~15.0.8",
37 | "lucide-react-native": "^0.518.0",
38 | "react": "19.1.0",
39 | "react-dom": "19.1.0",
40 | "react-native": "^0.81.5",
41 | "react-native-gesture-handler": "~2.28.0",
42 | "react-native-keyboard-controller": "1.18.5",
43 | "react-native-reanimated": "^4.2.0",
44 | "react-native-safe-area-context": "~5.6.0",
45 | "react-native-screen-transitions": "^2.3.3",
46 | "react-native-screens": "~4.16.0",
47 | "react-native-svg": "15.12.1",
48 | "react-native-web": "^0.21.0",
49 | "react-native-webview": "13.15.0",
50 | "react-native-worklets": "^0.7.1",
51 | "svg-path-properties": "^1.3.0",
52 | "worklets": "^0.0.1-alpha"
53 | },
54 | "devDependencies": {
55 | "@babel/core": "^7.25.2",
56 | "@types/react": "~19.1.10",
57 | "eslint": "^9.25.0",
58 | "eslint-config-expo": "~10.0.0",
59 | "typescript": "~5.9.2"
60 | },
61 | "private": true,
62 | "reanimated": {
63 | "staticFeatureFlags": {
64 | "ENABLE_SHARED_ELEMENT_TRANSITIONS": true
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/components/slack/message-box.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { View, StyleSheet } from "react-native";
3 | import { DATA } from "./config";
4 | import { ThemedText } from "../ThemedText";
5 | import { Image } from "expo-image";
6 | import { Link } from "expo-router";
7 |
8 | const MessageBox = () => {
9 | const saveImage = () => {
10 | console.log("Save image");
11 | };
12 |
13 | const handleSharePress = () => {
14 | console.log("Share image");
15 | };
16 |
17 | const handleBlockPress = () => {
18 | console.log("Block user");
19 | };
20 |
21 | return (
22 |
23 |
24 |
28 |
29 |
30 |
31 |
36 | {DATA.name}
37 |
38 |
39 |
40 |
44 |
45 |
46 |
51 |
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | const styles = StyleSheet.create({
60 | container: {
61 | flexDirection: "row",
62 | padding: 12,
63 | paddingTop: 24,
64 | gap: 10,
65 | },
66 | leftContainer: {},
67 | rightContainer: {
68 | flex: 1,
69 | gap: 2,
70 | },
71 | profileImage: {
72 | width: 36,
73 | height: 36,
74 | borderRadius: 8,
75 | borderCurve: "continuous",
76 | },
77 | senderName: {
78 | fontSize: 17,
79 | marginBottom: 5,
80 | },
81 | messageImage: {
82 | width: "100%",
83 | height: 450,
84 | borderRadius: 8,
85 | borderCurve: "continuous",
86 | },
87 | });
88 |
89 | export default MessageBox;
90 |
--------------------------------------------------------------------------------
/components/icons/index.tsx:
--------------------------------------------------------------------------------
1 | import Svg, { Path } from "react-native-svg";
2 |
3 | export interface IconProps {
4 | size?: number;
5 | color?: string;
6 | weight?: number;
7 | }
8 |
9 | export function LogoIcon(props: IconProps) {
10 | const { size = 218, color = "none" } = props;
11 | const width = (size / 218) * 249;
12 | return (
13 |
19 | );
20 | }
21 |
22 | export const ReloadIcon = (props: IconProps) => {
23 | const { size = 24, color = "none", weight = 2 } = props;
24 | return (
25 |
34 | );
35 | };
36 |
37 | export const GrokIcon = (props: IconProps) => {
38 | const { size = 48, color = "none", weight = 2 } = props;
39 | return (
40 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/app/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { StyleSheet, View } from "react-native";
3 | import Logo from "@/components/ui/Logo";
4 | import Button from "@/components/ui/Button";
5 | import { useThemeColor } from "@/hooks/useThemeColor";
6 | import { SafeAreaView } from "react-native-safe-area-context";
7 | import { useNavigation } from "expo-router";
8 | import { DrawerNavigationProp } from "@react-navigation/drawer";
9 | import TextArea from "@/components/TextArea";
10 | import { ThemedTextWrapper } from "@/components/ThemedText";
11 | import { KeyboardAvoidingView } from "react-native-keyboard-controller";
12 | import Slider from "@/components/Slider";
13 |
14 | export default function Index() {
15 | const text = useThemeColor("text");
16 | const bg = useThemeColor("background");
17 | const navigation = useNavigation>();
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 | {/*
26 |
45 | */}
46 |
47 |
59 |
60 |
61 | );
62 | }
63 |
64 | const styles = StyleSheet.create({
65 | container: {
66 | flex: 1,
67 | justifyContent: "center",
68 | alignItems: "center",
69 | },
70 | text: {
71 | fontSize: 24,
72 | fontWeight: "bold",
73 | },
74 | });
75 |
--------------------------------------------------------------------------------
/app/gesture-nav/index.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText } from "@/components/ThemedText";
2 | import { useThemeColor } from "@/hooks/useThemeColor";
3 | import { Gesture, GestureDetector } from "react-native-gesture-handler";
4 | import Animated, {
5 | useSharedValue,
6 | useAnimatedStyle,
7 | withSpring,
8 | } from "react-native-reanimated";
9 | import TopBar from "@/components/TopBar";
10 | import { View } from "react-native";
11 |
12 | const SPRING_CONFIG = {
13 | stiffness: 180,
14 | damping: 28,
15 | mass: 1,
16 | overshootClamping: true,
17 | restDisplacementThreshold: 0.000001,
18 | restSpeedThreshold: 0.000001,
19 | };
20 |
21 | export default function GestureNav() {
22 | const text = useThemeColor("text");
23 | const barColor = useThemeColor("barColor");
24 | const translateX = useSharedValue(0);
25 | const translateY = useSharedValue(0);
26 | const pressed = useSharedValue(false);
27 |
28 | const pan = Gesture.Pan()
29 | .onBegin(() => {
30 | pressed.value = true;
31 | })
32 | .onUpdate((e) => {
33 | translateX.value = e.translationX;
34 | // Apply a non-linear "stiffness" as translation increases
35 | const damped = e.translationY * 0.7;
36 | translateY.value = Math.max(
37 | 0,
38 | damped - 0.0005 * Math.pow(Math.max(0, damped), 2)
39 | );
40 | })
41 | .onEnd(() => {
42 | translateX.value = withSpring(0, SPRING_CONFIG);
43 | translateY.value = withSpring(0, SPRING_CONFIG);
44 | pressed.value = false;
45 | });
46 |
47 | const animatedStyle = useAnimatedStyle(() => ({
48 | transform: [{ translateY: translateY.value }],
49 | }));
50 |
51 | return (
52 |
53 |
59 |
64 |
65 |
73 |
79 | Pull Down
80 |
81 |
82 |
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/components/slack/message-bar.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet, Pressable, TextInput } from "react-native";
2 | import { GlassView } from "expo-glass-effect";
3 | import { useSafeAreaInsets } from "react-native-safe-area-context";
4 | import { ThemedTextWrapper } from "../ThemedText";
5 | import { Mic, Plus } from "lucide-react-native";
6 | import { useThemeColor } from "@/hooks/useThemeColor";
7 | import { KeyboardAvoidingView } from "react-native-keyboard-controller";
8 | import { ThemedView } from "../ThemedView";
9 |
10 | export default function MessageBar() {
11 | const { bottom } = useSafeAreaInsets();
12 | const text = useThemeColor("slackText");
13 |
14 | return (
15 |
20 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | const styles = StyleSheet.create({
53 | wrapper: {
54 | position: "absolute",
55 | bottom: 0,
56 | left: 0,
57 | right: 0,
58 | zIndex: 10,
59 | },
60 | container: {
61 | marginHorizontal: 16,
62 | borderRadius: 30,
63 | borderCurve: "continuous",
64 | height: 58,
65 | padding: 10,
66 | justifyContent: "center",
67 | flexDirection: "row",
68 | },
69 | button: {
70 | height: "100%",
71 | aspectRatio: 1,
72 | borderRadius: "50%",
73 | justifyContent: "center",
74 | alignItems: "center",
75 | opacity: 0.8,
76 | },
77 | buttonWrapper: {
78 | opacity: 1,
79 | },
80 | input: {
81 | flex: 1,
82 | padding: 8,
83 | fontSize: 16,
84 | },
85 | });
86 |
--------------------------------------------------------------------------------
/app/safari-bar/index.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet } from "react-native";
2 | import React from "react";
3 | import Animated, {
4 | useAnimatedScrollHandler,
5 | useSharedValue,
6 | } from "react-native-reanimated";
7 | import ProgressiveFade from "@/components/ProgressiveFade";
8 | import { ThemedView } from "@/components/ThemedView";
9 | import { ThemedText } from "@/components/ThemedText";
10 | import {
11 | SafeAreaView,
12 | useSafeAreaInsets,
13 | } from "react-native-safe-area-context";
14 | import Header from "@/components/safari-bar/header";
15 | import { DATA } from "@/components/safari-bar/config";
16 | import Article from "@/components/safari-bar/article";
17 | import Bar from "@/components/safari-bar/bar";
18 |
19 | const FADE_HEIGHT = 4;
20 | const { content, title, sections } = DATA;
21 |
22 | export default function SafariBar() {
23 | const { top } = useSafeAreaInsets();
24 | const scrollY = useSharedValue(0);
25 | const isScrollEnd = useSharedValue(true);
26 |
27 | const scrollHandler = useAnimatedScrollHandler({
28 | onBeginDrag: () => {
29 | isScrollEnd.value = false;
30 | console.log("Drag begin");
31 | },
32 | onScroll: (event) => {
33 | isScrollEnd.value = false;
34 | scrollY.value = Math.max(0, event.contentOffset.y);
35 | },
36 | onEndDrag: () => {
37 | isScrollEnd.value = true;
38 | console.log("Drag end");
39 | },
40 | });
41 | return (
42 |
43 | (
45 | {}} />
46 | )}
47 | style={styles.container}
48 | contentContainerStyle={[
49 | styles.content,
50 | { paddingTop: top + FADE_HEIGHT },
51 | ]}
52 | data={sections}
53 | renderItem={({ item, index }) => (
54 |
59 | )}
60 | onScroll={scrollHandler}
61 | // scrollEventThrottle={16}
62 | />
63 |
64 |
65 |
66 | );
67 | }
68 |
69 | const styles = StyleSheet.create({
70 | container: {
71 | flex: 1,
72 | alignSelf: "center",
73 | maxWidth: 640,
74 | },
75 | content: {
76 | paddingBottom: 20,
77 | },
78 | box: {
79 | height: 120,
80 | borderRadius: 10,
81 | width: "100%",
82 | backgroundColor: "#88888820",
83 | },
84 | });
85 |
--------------------------------------------------------------------------------
/components/drawer/DrawerItemList.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CommonActions,
3 | DrawerActions,
4 | useLinkBuilder,
5 | } from "@react-navigation/native";
6 | import * as React from "react";
7 |
8 | import { DrawerItem } from "./DrawerItem";
9 | import { DrawerContentComponentProps } from "@react-navigation/drawer";
10 | import { Href } from "expo-router";
11 |
12 | /**
13 | * Component that renders the navigation list in the drawer.
14 | */
15 | export function DrawerItemList({
16 | state,
17 | navigation,
18 | descriptors,
19 | }: DrawerContentComponentProps) {
20 | const { buildHref } = useLinkBuilder();
21 |
22 | const focusedRoute = state.routes[state.index];
23 | const focusedDescriptor = descriptors[focusedRoute.key];
24 | const focusedOptions = focusedDescriptor.options;
25 |
26 | const {
27 | drawerActiveTintColor,
28 | drawerInactiveTintColor,
29 | drawerActiveBackgroundColor,
30 | drawerInactiveBackgroundColor,
31 | } = focusedOptions;
32 |
33 | return state.routes.map((route, i) => {
34 | const focused = i === state.index;
35 |
36 | const onPress = () => {
37 | const event = navigation.emit({
38 | type: "drawerItemPress",
39 | target: route.key,
40 | canPreventDefault: true,
41 | });
42 |
43 | if (!event.defaultPrevented) {
44 | navigation.dispatch({
45 | ...(focused
46 | ? DrawerActions.closeDrawer()
47 | : CommonActions.navigate(route)),
48 | target: state.key,
49 | });
50 | }
51 | };
52 |
53 | const {
54 | title,
55 | drawerLabel,
56 | drawerIcon,
57 | drawerLabelStyle,
58 | drawerItemStyle,
59 | drawerAllowFontScaling,
60 | } = descriptors[route.key].options;
61 |
62 | return (
63 |
85 | );
86 | }) as React.ReactNode as React.ReactElement;
87 | }
88 |
--------------------------------------------------------------------------------
/components/re-captcha/underlay-text.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Svg, Text as SvgText, Circle } from "react-native-svg";
3 |
4 | const HORIZONTAL_SHIFT = 30;
5 | const DOT_OFFSET_X = 10;
6 | const DOT_OFFSET_Y = 10;
7 | const FONT_SIZE = 10;
8 | const DOT_RADIUS = 3;
9 |
10 | type DotPosition = "up" | "right" | "left" | "under";
11 |
12 | type NumberWithCoord = {
13 | num: number;
14 | x: number;
15 | y: number;
16 | dot: DotPosition;
17 | };
18 |
19 | const numbersWithCoords: NumberWithCoord[] = [
20 | { num: 1, x: 20 + HORIZONTAL_SHIFT, y: 150, dot: "right" },
21 | { num: 2, x: 20 + HORIZONTAL_SHIFT, y: 50, dot: "right" },
22 | { num: 3, x: 80 + HORIZONTAL_SHIFT, y: 130, dot: "up" },
23 | { num: 4, x: 110 + HORIZONTAL_SHIFT, y: 20, dot: "under" },
24 | { num: 5, x: 135 + HORIZONTAL_SHIFT, y: 125, dot: "up" },
25 | { num: 6, x: 175 + HORIZONTAL_SHIFT, y: 40, dot: "left" },
26 | { num: 7, x: 190 + HORIZONTAL_SHIFT, y: 150, dot: "left" },
27 | { num: 8, x: 15 + HORIZONTAL_SHIFT, y: 180, dot: "right" },
28 | ];
29 |
30 | function getTextCenterY(y: number) {
31 | return y - FONT_SIZE / 3;
32 | }
33 |
34 | function getDotCoords(
35 | x: number,
36 | y: number,
37 | position: DotPosition
38 | ): { cx: number; cy: number } {
39 | const centerY = getTextCenterY(y);
40 | switch (position) {
41 | case "left":
42 | return { cx: x - DOT_OFFSET_X, cy: centerY };
43 | case "right":
44 | return { cx: x + DOT_OFFSET_X, cy: centerY };
45 | case "up":
46 | return { cx: x, cy: centerY - DOT_OFFSET_Y };
47 | case "under":
48 | return { cx: x, cy: centerY + DOT_OFFSET_Y };
49 | default:
50 | return { cx: x, cy: y };
51 | }
52 | }
53 |
54 | type UnderlayTextProps = {
55 | textColor?: string;
56 | dotColor?: string;
57 | color?: string;
58 | };
59 |
60 | export default function UnderlayText({
61 | textColor,
62 | dotColor,
63 | color = "black",
64 | }: UnderlayTextProps) {
65 | return (
66 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/components/re-captcha/shimmer.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, View, ViewStyle, StyleProp } from "react-native";
2 | import Animated, {
3 | useAnimatedStyle,
4 | useSharedValue,
5 | withRepeat,
6 | withTiming,
7 | useAnimatedRef,
8 | measure,
9 | SharedValue,
10 | useAnimatedReaction,
11 | EasingFunction,
12 | Easing,
13 | } from "react-native-reanimated";
14 | import { useEffect, ReactNode } from "react";
15 | import { scheduleOnUI } from "react-native-worklets";
16 |
17 | type ShimmerProps = {
18 | children?: ReactNode;
19 | style?: StyleProp;
20 | color?: string;
21 | isPlaying?: SharedValue;
22 | rotateDeg?: `${string}deg`;
23 | duration?: number;
24 | easing?: EasingFunction;
25 | };
26 |
27 | const Shimmer = ({
28 | children,
29 | style,
30 | color = "#FFFFFF",
31 | isPlaying,
32 | rotateDeg = "45deg",
33 | duration = 1500,
34 | easing = Easing.inOut(Easing.ease),
35 | }: ShimmerProps) => {
36 | const shimmerProgress = useSharedValue(1);
37 | const width = useSharedValue(0);
38 | const containerRef = useAnimatedRef();
39 |
40 | useEffect(() => {
41 | scheduleOnUI(() => {
42 | const m = measure(containerRef);
43 | if (m) {
44 | width.value = m.width;
45 | }
46 | });
47 | return () => {
48 | shimmerProgress.value = 0;
49 | };
50 | }, []);
51 |
52 | useAnimatedReaction(
53 | () => isPlaying?.value,
54 | (playing, prevPlaying) => {
55 | if (playing === prevPlaying || !isPlaying) return;
56 | if (playing) {
57 | shimmerProgress.value = withRepeat(
58 | withTiming(0, { duration, easing }),
59 | -1,
60 | false
61 | );
62 | } else {
63 | shimmerProgress.value = 1;
64 | }
65 | }
66 | );
67 |
68 | const shimmerAnimatedStyle = useAnimatedStyle(() => {
69 | return {
70 | opacity: width.value === 0 ? 0 : 1,
71 | transform: [
72 | { rotate: rotateDeg },
73 | { translateY: (1.8 * shimmerProgress.value - 1) * width.value },
74 | ],
75 | };
76 | });
77 |
78 | const transparentHex = (hex: string) => {
79 | if (hex.length === 9) return hex.slice(0, 7) + "00";
80 | if (hex.length === 7) return hex + "00";
81 | return hex;
82 | };
83 |
84 | return (
85 |
86 | {children}
87 |
98 |
99 | );
100 | };
101 |
102 | const styles = StyleSheet.create({
103 | container: {
104 | overflow: "hidden",
105 | position: "relative",
106 | alignItems: "center",
107 | justifyContent: "center",
108 | flex: 1,
109 | // backgroundColor: "red",
110 | },
111 | shimmer: {
112 | position: "absolute",
113 | width: "150%",
114 | height: "40%",
115 | left: "-25%",
116 | top: "30%",
117 | },
118 | });
119 |
120 | export default Shimmer;
121 |
--------------------------------------------------------------------------------
/components/3dRotate.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet, StyleProp, ViewStyle } from "react-native";
2 | import React, { useImperativeHandle } from "react";
3 | import Animated, {
4 | SharedValue,
5 | useAnimatedStyle,
6 | useDerivedValue,
7 | useSharedValue,
8 | withSpring,
9 | } from "react-native-reanimated";
10 | import { Feedback } from "@/functions";
11 |
12 | export type Rotate3dHandle = {
13 | flip: () => void;
14 | flipTo: (to: "front" | "back") => void;
15 | };
16 |
17 | export type Rotate3dProps = {
18 | frontContent?: React.ReactNode;
19 | backContent?: React.ReactNode;
20 | progress?: SharedValue;
21 | itemStyle?: StyleProp;
22 | style?: StyleProp;
23 | ref?: React.RefObject;
24 | perspective?: number;
25 | rtl?: boolean;
26 | axis?: "x" | "y"; // <-- Add axis prop
27 | };
28 |
29 | const SPRING_CONFIG = {
30 | damping: 10,
31 | stiffness: 80,
32 | mass: 0.6,
33 | };
34 |
35 | export default function Rotate3d({
36 | progress: _progress,
37 | frontContent,
38 | backContent,
39 | itemStyle,
40 | style,
41 | ref,
42 | perspective = 800,
43 | rtl = true,
44 | axis = "x",
45 | }: Rotate3dProps) {
46 | const __progress = useSharedValue(0);
47 | const progress = _progress ?? __progress;
48 |
49 | const rotation = useDerivedValue(() => {
50 | return progress.value * 180 * (rtl ? -1 : 1);
51 | });
52 |
53 | const frontAnimatedStyle = useAnimatedStyle(() => {
54 | const rotateTransform =
55 | axis === "y"
56 | ? { rotateY: `${rotation.value}deg` }
57 | : { rotateX: `${rotation.value}deg` };
58 | return {
59 | transform: [{ perspective }, rotateTransform],
60 | pointerEvents: Math.abs(rotation.value) > 90 ? "none" : "auto",
61 | };
62 | });
63 |
64 | const backAnimatedStyle = useAnimatedStyle(() => {
65 | const rotateTransform =
66 | axis === "y"
67 | ? { rotateY: `${rotation.value + 180}deg` }
68 | : { rotateX: `${rotation.value + 180}deg` };
69 | return {
70 | transform: [{ perspective }, rotateTransform],
71 | pointerEvents: Math.abs(rotation.value) <= 90 ? "none" : "auto",
72 | };
73 | });
74 |
75 | const flipCard = () => {
76 | progress.value = withSpring(
77 | Math.round(progress.value) === 0 ? 1 : 0,
78 | SPRING_CONFIG
79 | );
80 | Feedback.soft();
81 | };
82 |
83 | const flipTo = (to: "front" | "back") => {
84 | const targetValue = to === "front" ? 0 : 1;
85 | progress.value = withSpring(targetValue, SPRING_CONFIG);
86 | Feedback.soft();
87 | };
88 |
89 | useImperativeHandle(
90 | ref,
91 | () => ({
92 | flip: flipCard,
93 | flipTo,
94 | }),
95 | [flipCard]
96 | );
97 |
98 | return (
99 |
100 |
103 | {frontContent}
104 |
105 |
108 | {backContent}
109 |
110 |
111 | );
112 | }
113 |
114 | const styles = StyleSheet.create({
115 | flipCard: {},
116 | flipCardItem: {
117 | backfaceVisibility: "hidden",
118 | height: "100%",
119 | width: "100%",
120 | position: "absolute",
121 | // transformOrigin: "bottom center",
122 | },
123 | text: {
124 | fontSize: 24,
125 | },
126 | });
127 |
--------------------------------------------------------------------------------
/components/re-captcha/card.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet } from "react-native";
2 | import React from "react";
3 | import Animated, {
4 | useAnimatedStyle,
5 | useDerivedValue,
6 | useSharedValue,
7 | withSpring,
8 | } from "react-native-reanimated";
9 | import CheckBox from "../ui/check-box";
10 | import { ThemedViewWrapper } from "../ThemedView";
11 | import { Image } from "expo-image";
12 | import { useThemeColor } from "@/hooks/useThemeColor";
13 | import { ThemedText } from "../ThemedText";
14 | import { ReCaptchaProps, SPRING_CONFIG } from "./config";
15 | import { Feedback } from "@/functions";
16 |
17 | export default function CaptchaCard({
18 | shrinkProgress,
19 | isVerified,
20 | }: ReCaptchaProps) {
21 | const checkBoxInitialColor = useThemeColor("captchaCheckboxInitialBg");
22 | const captchaCheckboxBg = useThemeColor("captchaCheckboxBg");
23 | const systemBlue = useThemeColor("systemBlue");
24 | const disabled = useSharedValue(false);
25 | const checkedSV = useSharedValue(false);
26 |
27 | useDerivedValue(() => {
28 | if (isVerified.value) {
29 | disabled.value = false;
30 | shrinkProgress.value = withSpring(0, SPRING_CONFIG);
31 | }
32 | });
33 |
34 | const animatedCardStyle = useAnimatedStyle(() => {
35 | const scale = 1 - shrinkProgress.value * 0.3;
36 | return {
37 | transform: [{ scale }],
38 | };
39 | });
40 |
41 | return (
42 |
43 |
44 |
45 | {
54 | Feedback.soft();
55 | disabled.value = true;
56 | checkedSV.value = true;
57 | shrinkProgress.value = withSpring(checked ? 1 : 0, SPRING_CONFIG);
58 | }}
59 | />
60 |
61 | I'm not a robot
62 |
63 |
64 |
65 |
71 | reCAPTCHA
72 |
73 |
74 |
75 | );
76 | }
77 |
78 | const styles = StyleSheet.create({
79 | card: {
80 | width: 310,
81 | padding: 18,
82 | height: 74,
83 | backgroundColor: "#fff",
84 | borderRadius: 38,
85 | alignItems: "center",
86 | justifyContent: "space-between",
87 | flexDirection: "row",
88 | // transformOrigin: "bottom center",
89 | },
90 | image: {
91 | width: 48,
92 | aspectRatio: 1,
93 | },
94 | checkBox: {
95 | boxShadow: "0 3px 6px rgba(0,0,0,0.1)",
96 | },
97 | captchLogo: {
98 | alignItems: "center",
99 | justifyContent: "center",
100 | },
101 | text: {
102 | fontSize: 5,
103 | marginTop: -5,
104 | fontFamily: "Menlo",
105 | opacity: 0.3,
106 | },
107 | content: {
108 | flexDirection: "row",
109 | gap: 12,
110 | alignItems: "center",
111 | },
112 | title: {
113 | fontSize: 22,
114 | opacity: 0.65,
115 | fontWeight: "600",
116 | fontFamily: "ui-rounded",
117 | },
118 | });
119 |
--------------------------------------------------------------------------------
/components/slack/header.ios.tsx:
--------------------------------------------------------------------------------
1 | import { Sparkles } from "lucide-react-native";
2 | import { View, Text, StyleSheet, Modal, Pressable } from "react-native";
3 | import { NativeStackHeaderRightProps } from "react-native-screen-transitions";
4 | import { ThemedTextWrapper } from "../ThemedText";
5 | import { Button, ContextMenu, Host } from "@expo/ui/swift-ui";
6 | import { useSafeAreaInsets } from "react-native-safe-area-context";
7 | import { useState } from "react";
8 | import Island, { Cords } from "./island";
9 | import {
10 | measure,
11 | SharedValue,
12 | useAnimatedRef,
13 | useDerivedValue,
14 | useSharedValue,
15 | } from "react-native-reanimated";
16 | import { ANIMATION_DELAY } from "./config";
17 |
18 | export default function HeaderTitle({}: {
19 | children: React.ReactNode;
20 | tintColor?: string;
21 | style?: any;
22 | }) {
23 | const [modalVisible, setModalVisible] = useState(false);
24 | const islandRef = useAnimatedRef();
25 | const cords = useSharedValue({ x: 0, y: 0, width: 0, height: 0 });
26 |
27 | useDerivedValue(() => {
28 | const measured = measure(islandRef);
29 | if (measured !== null) {
30 | const { x, y, width, height, pageX, pageY } = measured;
31 | // console.log({ x, y, width, height, pageX, pageY });
32 | cords.value = { x: pageX, y: pageY, width, height };
33 | } else {
34 | console.warn("measure: could not measure view");
35 | }
36 | });
37 |
38 | return (
39 | <>
40 | {
42 | setModalVisible(true);
43 | }}
44 | containerRef={islandRef}
45 | visible={!modalVisible}
46 | />
47 |
52 | >
53 | );
54 | }
55 |
56 | const ModalHeader = ({
57 | visible,
58 | setVisible,
59 | cords,
60 | }: {
61 | visible: boolean;
62 | setVisible: (visible: boolean) => void;
63 | cords: SharedValue;
64 | }) => {
65 | const { top } = useSafeAreaInsets();
66 | const opened = useSharedValue(false);
67 |
68 | const onClose = () => {
69 | opened.value = false;
70 | setTimeout(() => {
71 | setVisible(false);
72 | }, ANIMATION_DELAY);
73 | };
74 |
75 | return (
76 |
83 |
84 |
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | export const HeaderRight = ({}: NativeStackHeaderRightProps) => {
92 | return (
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | const styles = StyleSheet.create({
112 | rightContainer: {
113 | width: 36,
114 | aspectRatio: 1,
115 | alignItems: "center",
116 | justifyContent: "center",
117 | },
118 | underlay: {
119 | ...StyleSheet.absoluteFillObject,
120 | // backgroundColor: "red",
121 | },
122 | });
123 |
--------------------------------------------------------------------------------
/components/safari-bar/config.ts:
--------------------------------------------------------------------------------
1 | export const DATA = {
2 | title: "Clean Mobile App UI Design",
3 | content:
4 | "Great UI design is more than just looks—it improves usability, accessibility, and user experience. This guide covers essential principles for crafting clean and functional mobile app interfaces.",
5 | sections: [
6 | {
7 | title: "Why Clean UI Matters",
8 | content: [
9 | "A cluttered UI confuses users, while a clean design makes interactions effortless. By focusing on simplicity, spacing, and consistency, you create an intuitive experience that improves usability and engagement.",
10 | "Top companies prioritize clean UI to boost user engagement and retention. A well-structured interface reduces cognitive load and enhances user satisfaction. Understanding the psychology behind clean design helps you craft more user-friendly and scalable interfaces.",
11 | ],
12 | },
13 | {
14 | title: "Typography and Readability",
15 | content: [
16 | "Typography sets the tone for your app. Choose readable fonts like Inter or Roboto, maintain proper spacing, and ensure a clear text hierarchy to create a pleasant reading experience.",
17 | "Headings should stand out without overwhelming the screen. Proper line height, font weight, and spacing improve readability. Keep body text at a comfortable size, typically between 14px and 16px, and avoid excessive bold or italic styles that can strain the eyes.",
18 | "Consider accessibility in typography choices. Ensure contrast ratios meet WCAG standards, avoid using only color to convey meaning, and use dynamic scaling for different screen sizes.",
19 | ],
20 | },
21 | {
22 | title: "Spacing and Layout",
23 | content: [
24 | "White space (negative space) helps declutter the UI and improve focus. Elements need breathing room to enhance readability and usability, making content easier to scan.",
25 | "Use consistent padding and margins. Following an 8px or 4px spacing system helps maintain a structured and balanced layout across different screen sizes and resolutions.",
26 | ],
27 | },
28 | {
29 | title: "Color and Contrast",
30 | content: [
31 | "Colors impact user perception. A simple, well-balanced color scheme enhances aesthetics and usability, reinforcing brand identity while improving readability.",
32 | "Use high contrast for readability (e.g., dark text on a light background). Stick to a limited color palette and avoid unnecessary gradients or excessive shadows that can reduce clarity and visual hierarchy.",
33 | "Color psychology plays a role in UX. Cool tones evoke calmness, warm tones create energy, and neutral shades provide balance. Ensure accessibility by testing color contrast and avoiding color combinations that hinder readability for visually impaired users.",
34 | ],
35 | },
36 | {
37 | title: "Touch and Interaction Design",
38 | content: [
39 | "Buttons, gestures, and animations should feel smooth and natural. Ensure touch targets are at least 44x44 pixels for accessibility, making interactions effortless and reducing frustration.",
40 | "Use motion subtly—smooth transitions and microinteractions should enhance the experience, not distract from it. Animations should provide feedback, such as button press effects or loading indicators, to guide users.",
41 | ],
42 | },
43 | {
44 | title: "Icons and Imagery",
45 | content: [
46 | "Icons should be intuitive and universally recognizable. They help users navigate and understand actions quickly. Use a consistent style and size for icons throughout the app.",
47 | "Images should be high quality and relevant. They can enhance the visual appeal and provide context, but avoid overloading the interface with too many images, which can slow down performance.",
48 | "Optimize images for different screen sizes and resolutions. Use responsive images and consider using vector graphics (SVGs) for scalability and clarity on various devices.",
49 | ],
50 | },
51 | ],
52 | };
53 |
--------------------------------------------------------------------------------
/components/untitled/card.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet } from "react-native";
2 | import React from "react";
3 | import { ThemedText, ThemedTextWrapper } from "../ThemedText";
4 | import { Colors } from "@/constants/Colors";
5 | import { Link } from "expo-router";
6 | import Ionicons from "@expo/vector-icons/Ionicons";
7 | import PressableBounce from "../PresableBounce";
8 |
9 | export const UntitledCardMini = ({
10 | name = "untitled project",
11 | author = "Dotjs",
12 | }: {
13 | name?: string;
14 | author?: string;
15 | }) => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 | {name}
24 |
25 |
26 |
27 |
28 |
29 |
30 | {author}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export const UntitledCardMiniWrapper = ({
44 | children,
45 | }: {
46 | children?: React.ReactNode;
47 | }) => {
48 | return {children};
49 | };
50 |
51 | export const UntitledCardLarge = ({
52 | name = "untitled project",
53 | author = "Dotjs",
54 | }: {
55 | name?: string;
56 | author?: string;
57 | }) => {
58 | return (
59 |
60 |
61 |
62 |
63 |
64 | {name}
65 |
66 |
67 |
68 |
69 |
70 |
71 | {author}
72 |
73 |
74 |
75 |
76 |
77 | );
78 | };
79 |
80 | const styles = StyleSheet.create({
81 | container: {
82 | width: "50%",
83 | },
84 | content: {
85 | marginHorizontal: 22,
86 | alignItems: "center",
87 | },
88 | title: {
89 | fontSize: 17.2,
90 | },
91 | titleLarge: {
92 | fontSize: 28,
93 | letterSpacing: -0.5,
94 | opacity: 0.7,
95 | },
96 | subtitle: {
97 | fontSize: 14,
98 | flex: 1,
99 | },
100 | fade: {
101 | opacity: 0.6,
102 | },
103 | boxWrapper: {
104 | borderRadius: 16,
105 | borderCurve: "continuous",
106 | overflow: "hidden",
107 | },
108 | box: {
109 | width: "100%",
110 | flex: 1,
111 | aspectRatio: 1,
112 | borderRadius: 18,
113 | borderCurve: "continuous",
114 | experimental_backgroundImage: `linear-gradient(135deg, ${Colors.dark.untitledGradient2} 0%, ${Colors.dark.untitledGradient1} 80%)`,
115 | boxShadow: "0px 5px 10px rgba(0,0,0,0.1)",
116 | },
117 | boxLarge: {
118 | width: "90%",
119 | margin: 12,
120 | },
121 | details: {
122 | width: "100%",
123 | marginTop: 12,
124 | gap: 4,
125 | },
126 | cluster: {
127 | flexDirection: "row",
128 | alignItems: "center",
129 | gap: 4,
130 | },
131 | wrapper: {
132 | padding: 10,
133 | rowGap: 22,
134 | // backgroundColor: "red",
135 | flexWrap: "wrap",
136 | flexDirection: "row",
137 | },
138 | });
139 |
--------------------------------------------------------------------------------
/components/notify/card.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollView, StyleSheet, TextInput, View } from "react-native";
2 | import { ThemedText } from "../ThemedText";
3 | import { ThemedViewWrapper } from "../ThemedView";
4 | import Animated, {
5 | SharedValue,
6 | useAnimatedStyle,
7 | withSpring,
8 | } from "react-native-reanimated";
9 | import { BlurView } from "expo-blur";
10 | import { Image } from "expo-image";
11 | import { MessageType } from "./type";
12 |
13 | type CardGlobalProps = {
14 | shown: SharedValue;
15 | };
16 |
17 | const useOpacityAnimation = (shown: SharedValue) => {
18 | return useAnimatedStyle(() => {
19 | return {
20 | opacity: withSpring(shown.value ? 1 : 0),
21 | };
22 | });
23 | };
24 |
25 | export const CardPeek = ({
26 | text,
27 | options,
28 | shown,
29 | }: MessageType & CardGlobalProps) => {
30 | const animatedStyle = useOpacityAnimation(shown);
31 |
32 | return (
33 |
36 |
37 |
38 |
42 |
43 |
44 |
50 | {text}
51 |
52 |
57 | {options?.description}
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | const CardHeader = ({ title }: { title: string }) => {
66 | return (
67 |
68 | {title}
69 |
70 | );
71 | };
72 |
73 | export const CardExpanded = ({
74 | children,
75 | shown,
76 | }: { children?: React.ReactNode } & CardGlobalProps) => {
77 | const animatedStyle = useOpacityAnimation(shown);
78 |
79 | return (
80 |
81 | {children}
82 |
83 | );
84 | };
85 |
86 | export const CardHandle = ({ shown }: CardGlobalProps) => {
87 | const animatedStyle = useOpacityAnimation(shown);
88 |
89 | return ;
90 | };
91 |
92 | const styles = StyleSheet.create({
93 | container: {
94 | flex: 1,
95 | },
96 | peekWrapper: {
97 | justifyContent: "center",
98 | },
99 | peekContainer: {
100 | padding: 12,
101 | flexDirection: "row",
102 | // backgroundColor: "blue",
103 | alignItems: "center",
104 | gap: 8,
105 | maxHeight: 78,
106 | },
107 | text: {},
108 | handle: {
109 | width: 40,
110 | height: 4,
111 | borderRadius: 2,
112 | borderCurve: "continuous",
113 | alignSelf: "center",
114 | position: "absolute",
115 | bottom: 5,
116 | backgroundColor: "#88888888",
117 | },
118 | header: {
119 | position: "absolute",
120 | top: 0,
121 | left: 0,
122 | right: 0,
123 | height: 48,
124 | alignItems: "center",
125 | justifyContent: "center",
126 | },
127 | expanded: {
128 | flex: 1,
129 | },
130 | expandedContainer: {
131 | ...StyleSheet.absoluteFillObject,
132 | margin: 1,
133 | overflow: "hidden",
134 | borderRadius: 24,
135 | borderCurve: "continuous",
136 | },
137 | imageWrapper: {
138 | borderRadius: "50%",
139 | overflow: "hidden",
140 | },
141 | image: {
142 | height: "100%",
143 | aspectRatio: 1,
144 | backgroundColor: "#88888888",
145 | },
146 | peekContent: {
147 | flex: 1,
148 | gap: 2,
149 | },
150 | title: {},
151 | description: {
152 | fontSize: 14,
153 | opacity: 0.9,
154 | },
155 | });
156 |
--------------------------------------------------------------------------------
/app/in-app-notif/index.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Pressable, TextInput, ScrollView } from "react-native";
2 | import React from "react";
3 | import { ThemedView } from "@/components/ThemedView";
4 | import { ThemedText, ThemedTextWrapper } from "@/components/ThemedText";
5 | import { MessageType } from "@/components/notify/type";
6 | import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
7 | import { useNotify } from "@/components/notify";
8 | import PressableBounce from "@/components/PresableBounce";
9 | import TextArea from "@/components/TextArea";
10 |
11 | const isLiquidGlass = isLiquidGlassAvailable();
12 |
13 | const ExpandedContent = () => {
14 | return (
15 |
26 |
27 | Expanded content goes here
28 |
29 |
30 | You can put any React Node
31 |
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | const notifPayload: MessageType = {
40 | text: "Welcome",
41 | options: {
42 | description: "This is a normal notification",
43 | action: {
44 | label: "OK",
45 | onClick: () => {
46 | console.log("Notification action clicked");
47 | },
48 | },
49 | },
50 | };
51 |
52 | const expandableNotifPayload: MessageType = {
53 | text: "Expandable",
54 | options: {
55 | description: "This is an expandable notification",
56 | expandedChild: ,
57 | action: {
58 | label: "OK",
59 | onClick: () => {
60 | console.log("Expandable notification action clicked");
61 | },
62 | },
63 | },
64 | };
65 |
66 | export default function Index() {
67 | const { notify } = useNotify();
68 |
69 | return (
70 |
71 | notify(notifPayload.text, notifPayload.options)}
73 | label="Send Normal"
74 | />
75 |
77 | notify(expandableNotifPayload.text, expandableNotifPayload.options)
78 | }
79 | label="Send Expandable"
80 | />
81 |
82 | );
83 | }
84 |
85 | const NotifyButton = ({
86 | onPress,
87 | label,
88 | }: {
89 | onPress: () => void;
90 | label: string;
91 | }) => {
92 | const PressableComponent = isLiquidGlass ? Pressable : PressableBounce;
93 | const Wrapper = isLiquidGlass ? GlassView : ThemedView;
94 | const WrapperProps = isLiquidGlass
95 | ? { style: styles.glass, isInteractive: true }
96 | : { colorName: "barColor" as const, style: styles.glass };
97 |
98 | return (
99 |
100 |
101 | {label}
102 |
103 |
104 | );
105 | };
106 |
107 | const styles = StyleSheet.create({
108 | container: {
109 | flex: 1,
110 | justifyContent: "center",
111 | alignItems: "center",
112 | gap: 12,
113 | experimental_backgroundImage:
114 | "linear-gradient(120deg, #ff006624, #00ff8824)",
115 | },
116 | title: {
117 | fontSize: 23,
118 | fontWeight: "bold",
119 | fontFamily: "ui-rounded",
120 | lineHeight: 32,
121 | },
122 | description: {
123 | fontSize: 16,
124 | fontFamily: "ui-rounded",
125 | },
126 | glass: {
127 | borderRadius: 40,
128 | borderCurve: "continuous",
129 | alignItems: "center",
130 | justifyContent: "center",
131 | },
132 | buttonText: {
133 | padding: 16,
134 | fontSize: 18,
135 | textAlign: "center",
136 | width: 280,
137 | },
138 | input: {
139 | marginTop: 16,
140 | width: "100%",
141 | height: 45,
142 | borderColor: "#88888850",
143 | borderWidth: 1,
144 | borderRadius: 10,
145 | paddingHorizontal: 10,
146 | borderCurve: "continuous",
147 | },
148 | });
149 |
--------------------------------------------------------------------------------
/app/open-gift/index.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet, Pressable } from "react-native";
2 | import React, { useRef } from "react";
3 | import Rotate3d, { Rotate3dHandle, Rotate3dProps } from "@/components/3dRotate";
4 | import { ThemedText, ThemedTextWrapper } from "@/components/ThemedText";
5 | import PressableBounce from "@/components/PresableBounce";
6 | import { useSharedValue } from "react-native-reanimated";
7 | import { useThemeColor } from "@/hooks/useThemeColor";
8 | import { ArrowLeft, RotateCcw } from "lucide-react-native";
9 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
10 | import DrawPad, { DrawPadHandle } from "expo-drawpad";
11 | import { Image, ImageBackground } from "expo-image";
12 | import { GlassView } from "expo-glass-effect";
13 | import { ThemedView } from "@/components/ThemedView";
14 |
15 | export default function Index() {
16 | const rotateRef = useRef(null);
17 |
18 | return (
19 |
20 | rotateRef.current?.flipTo("back")} />
25 | }
26 | backContent={
27 | rotateRef.current?.flipTo("front")} />
28 | }
29 | axis="y"
30 | />
31 |
32 | );
33 | }
34 |
35 | const ItemWrapper = ({
36 | children,
37 | style,
38 | }: {
39 | children: React.ReactNode;
40 | style?: any;
41 | }) => {
42 | const text = useThemeColor("text");
43 | const itemStyle = { borderColor: text + "24" };
44 | return {children};
45 | };
46 |
47 | const FrontContent = ({ goForward }: { goForward: () => void }) => (
48 |
49 |
56 |
60 |
61 |
62 | );
63 |
64 | const BackContent = ({ goBack }: { goBack: () => void }) => {
65 | const padRef = useRef(null);
66 | const pathLength = useSharedValue(0);
67 | const text = useThemeColor("slackText");
68 |
69 | const clearPad = () => {
70 | padRef.current?.erase();
71 | };
72 |
73 | return (
74 |
75 |
76 |
77 |
78 | );
79 | };
80 |
81 | const Button = ({
82 | onPress,
83 | title,
84 | }: {
85 | onPress?: () => void;
86 | title: string;
87 | }) => {
88 | const text = useThemeColor("text");
89 | return (
90 |
91 |
95 |
96 | {title}
97 |
98 |
99 |
100 | );
101 | };
102 |
103 | const styles = StyleSheet.create({
104 | container: {
105 | flexGrow: 1,
106 | alignItems: "center",
107 | justifyContent: "center",
108 | },
109 | rotateCard: {
110 | width: 320,
111 | height: 364,
112 | margin: 40,
113 | },
114 | itemStyle: {
115 | flex: 1,
116 | borderRadius: 30,
117 | borderCurve: "continuous",
118 | borderWidth: 1.8,
119 | boxShadow: "0 0px 8px rgba(0,0,0,0.05)",
120 | padding: 18,
121 | overflow: "hidden",
122 | },
123 | backCard: {
124 | borderStyle: "dashed",
125 | boxShadow: "none",
126 | },
127 | frontCard: {
128 | gap: 12,
129 | alignItems: "center",
130 | },
131 | title: {
132 | fontSize: 22,
133 | },
134 | gift_box: {
135 | flex: 1,
136 | aspectRatio: 1,
137 | margin: 4,
138 | },
139 | buttonWrapper: {
140 | borderRadius: 22,
141 | borderCurve: "continuous",
142 | width: "100%",
143 | },
144 | button: {
145 | height: 52,
146 | alignItems: "center",
147 | justifyContent: "center",
148 | },
149 | });
150 |
--------------------------------------------------------------------------------
/app/3d-card-scrolling/index.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, ScrollView, StyleSheet } from "react-native";
2 | import React from "react";
3 | import Animated, {
4 | Extrapolation,
5 | interpolate,
6 | SharedValue,
7 | useAnimatedScrollHandler,
8 | useAnimatedStyle,
9 | useSharedValue,
10 | } from "react-native-reanimated";
11 | import { ThemedView, ThemedViewWrapper } from "@/components/ThemedView";
12 | import { ThemedText } from "@/components/ThemedText";
13 | import { GlassView } from "expo-glass-effect";
14 |
15 | const LENGTH = 5;
16 |
17 | export default function Index() {
18 | const scrollY = useSharedValue(0);
19 | const scrollHeight = useSharedValue(0);
20 |
21 | const scrollHandler = useAnimatedScrollHandler({
22 | onScroll: (event) => {
23 | scrollY.value = event.contentOffset.y;
24 | scrollHeight.value =
25 | event.contentSize.height - event.layoutMeasurement.height;
26 | },
27 | });
28 | return (
29 |
30 |
42 | {Array.from({ length: LENGTH }).map((_, index) => (
43 |
49 | ))}
50 |
51 |
52 | );
53 | }
54 |
55 | const Card = ({
56 | scrollY,
57 | scrollHeight,
58 | index,
59 | }: {
60 | scrollY: SharedValue;
61 | scrollHeight: SharedValue;
62 | index?: number;
63 | }) => {
64 | const animatedStyle = useAnimatedStyle(() => {
65 | const h = scrollHeight.value;
66 | console.log(h);
67 | const isBelow = h <= 50;
68 |
69 | const indexFactor = 1 - (1 - (index ?? 0)) * (0.015 / (LENGTH / 5));
70 | const rotateX =
71 | interpolate(
72 | scrollY.value,
73 | isBelow ? [-200, 0, h + 200] : [-200, 0, h, h + 200],
74 | isBelow ? [-80, -65, -70] : [-80, -75, -70, -60]
75 | // Extrapolation.EXTEND
76 | ) *
77 | Math.max(1, (5 - LENGTH) * 0.4) *
78 | indexFactor;
79 |
80 | return {
81 | transform: [
82 | {
83 | perspective: 1600,
84 | },
85 | { rotateX: `${rotateX}deg` },
86 | { scaleY: 1.6 },
87 | { scaleX: 0.78 },
88 | ],
89 | zIndex: 1,
90 | };
91 | });
92 |
93 | return (
94 |
102 |
103 |
109 | {/* Hello There */}
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | );
119 | };
120 |
121 | const styles = StyleSheet.create({
122 | container: {
123 | flex: 1,
124 | },
125 | card: {
126 | height: 600,
127 | width: "100%",
128 | boxShadow: "0 0 60px rgba(0, 0, 0, 0.15)",
129 | borderRadius: 36,
130 | padding: 16,
131 | borderCurve: "continuous",
132 | gap: 12,
133 | // backgroundColor: "grey",
134 | // experimental_backgroundImage:
135 | // "linear-gradient(to bottom, #888888 0%, #88888880 100%)",
136 | },
137 | cardWrapper: {
138 | // width: "100%",
139 | // overflow: "hidden",
140 | },
141 | box: {
142 | height: 160,
143 | borderRadius: 20,
144 | width: "100%",
145 | borderCurve: "continuous",
146 | },
147 | dotWrapper: {
148 | flexDirection: "row",
149 | gap: 4,
150 | },
151 | dot: {
152 | width: 36,
153 | aspectRatio: 1,
154 | borderRadius: "50%",
155 | marginHorizontal: 4,
156 | },
157 | textBlock: {
158 | height: 36,
159 | flex: 1,
160 | borderRadius: 18,
161 | },
162 | });
163 |
--------------------------------------------------------------------------------
/components/slack/island.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | GestureResponderEvent,
3 | Pressable,
4 | StyleProp,
5 | StyleSheet,
6 | useWindowDimensions,
7 | ViewStyle,
8 | } from "react-native";
9 | import React, { useEffect } from "react";
10 | import { GlassView } from "expo-glass-effect";
11 | import Animated, {
12 | SharedValue,
13 | useAnimatedStyle,
14 | useSharedValue,
15 | } from "react-native-reanimated";
16 | import {
17 | BarButton,
18 | BarOptions,
19 | ButtonCluster,
20 | ButtonSeperator,
21 | ContentContainer,
22 | Dp,
23 | InfoBar,
24 | ItemProps,
25 | QuickActions,
26 | } from ".";
27 | import {
28 | CLOSED_HEIGHT,
29 | DATA,
30 | FULL_HEIGHT,
31 | OPENED_PAD,
32 | PADDING,
33 | applySpring,
34 | } from "./config";
35 | import Ionicons from "@expo/vector-icons/Ionicons";
36 | import { FileText, Layers, Settings, UserRound } from "lucide-react-native";
37 |
38 | export type Cords = { x: number; y: number; width: number; height: number };
39 |
40 | const AnimatedGlassView = Animated.createAnimatedComponent(GlassView);
41 |
42 | export default function Island({
43 | onPress,
44 | modal = false,
45 | containerRef,
46 | visible = true,
47 | cords,
48 | opened: openedProp,
49 | onClose,
50 | }: {
51 | onPress?: (e: GestureResponderEvent) => void;
52 | onClose?: (e: GestureResponderEvent) => void;
53 | modal?: boolean;
54 | containerRef?: React.RefObject;
55 | visible?: boolean;
56 | cords?: SharedValue;
57 | opened?: SharedValue;
58 | }) {
59 | const { width } = useWindowDimensions();
60 | const FULL_WIDTH = width - PADDING * 2;
61 | const _opened = useSharedValue(false);
62 | const opened = openedProp || _opened;
63 |
64 | const sharedProp: ItemProps = {
65 | opened,
66 | modal,
67 | };
68 |
69 | useEffect(() => {
70 | opened.value = true;
71 | }, []);
72 |
73 | const animatedStyle = useAnimatedStyle(() => {
74 | const { x, width } = cords?.value || {};
75 | const isOpened = opened.value;
76 |
77 | const modalStyle: StyleProp = modal
78 | ? {
79 | position: "absolute",
80 | left: applySpring(isOpened ? PADDING : x || 0),
81 | width: applySpring(isOpened ? FULL_WIDTH : width || 0),
82 | height: applySpring(isOpened ? FULL_HEIGHT : CLOSED_HEIGHT),
83 | paddingTop: applySpring(isOpened ? OPENED_PAD : 0),
84 | }
85 | : {};
86 | return {
87 | opacity: visible ? 1 : 0,
88 | ...modalStyle,
89 | };
90 | });
91 |
92 | return (
93 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | }
110 | label="Messages"
111 | active
112 | onPress={onClose}
113 | />
114 | }
116 | label="Files"
117 | onPress={onClose}
118 | />
119 | }
121 | label="Canvas"
122 | onPress={onClose}
123 | />
124 |
125 | }
127 | label="Call"
128 | rightButton
129 | onPress={onClose}
130 | />
131 | }
133 | label="Settings"
134 | rightButton
135 | onPress={onClose}
136 | />
137 |
138 |
139 |
140 |
141 | );
142 | }
143 |
144 | const styles = StyleSheet.create({
145 | glass: {
146 | flex: 1,
147 | // width: 400,
148 | height: CLOSED_HEIGHT,
149 | marginRight: 48,
150 | borderCurve: "continuous",
151 | borderRadius: CLOSED_HEIGHT / 2,
152 | },
153 | topBar: {
154 | width: "100%",
155 | height: CLOSED_HEIGHT,
156 | flexDirection: "row",
157 | alignItems: "center",
158 | gap: 4,
159 | },
160 | });
161 |
--------------------------------------------------------------------------------
/components/DrawerContent.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useEffect, useState } from "react";
2 | import {
3 | DrawerContentComponentProps,
4 | // DrawerItemList,
5 | useDrawerStatus,
6 | } from "@react-navigation/drawer";
7 | import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
8 | import { ThemedText } from "./ThemedText";
9 | import { SafeAreaView } from "react-native-safe-area-context";
10 | import { Image } from "expo-image";
11 | import FontAwesome6 from "@expo/vector-icons/FontAwesome6";
12 | import TextLink from "./ui/TextLink";
13 | import config from "@/constants/config.json";
14 | import { Home } from "lucide-react-native";
15 | import { useThemeColor } from "@/hooks/useThemeColor";
16 | import { useFocusEffect, useRouter } from "expo-router";
17 | import { Feedback } from "@/functions";
18 | import { DrawerItemList } from "./drawer/DrawerItemList";
19 |
20 | const DrawerContent = memo((props: DrawerContentComponentProps) => {
21 | const demos = (props.state?.routes?.length || 0) - 1 || 0;
22 | const routeNames = props.state?.routes.map((route) => route.name) || [];
23 | // console.log("Route names:", routeNames);
24 |
25 | const status = useDrawerStatus();
26 | const [focused, setFocused] = useState(false);
27 |
28 | useEffect(() => {
29 | if (focused) {
30 | Feedback.soft();
31 | }
32 | }, [status]);
33 |
34 | useFocusEffect(
35 | useCallback(() => {
36 | setFocused(true);
37 | return () => {
38 | setFocused(false);
39 | };
40 | }, [])
41 | );
42 |
43 | return (
44 |
45 |
46 |
50 |
51 |
52 |
53 |
54 | );
55 | });
56 |
57 | const Header = ({ routes }: { routes: number }) => {
58 | const text = useThemeColor("text");
59 | const textColor = text + "EA";
60 | const router = useRouter();
61 |
62 | const handlePress = () => {
63 | router.push("/");
64 | };
65 |
66 | return (
67 |
75 |
76 |
77 |
78 | Home
79 |
80 |
81 |
82 | {routes}
83 | {routes > 1 ? " demos" : " demo"}
84 |
85 |
86 | );
87 | };
88 |
89 | const DrawerFooter = () => {
90 | return (
91 |
92 |
93 |
97 | Solarin
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | export default DrawerContent;
112 |
113 | const styles = StyleSheet.create({
114 | container: {
115 | flex: 1,
116 | },
117 | scrollViewContent: {
118 | padding: 12,
119 | gap: 6,
120 | },
121 | header: {
122 | marginHorizontal: 12,
123 | paddingHorizontal: 2,
124 | flexDirection: "row",
125 | alignItems: "center",
126 | justifyContent: "space-between",
127 | borderBottomWidth: 1,
128 | paddingBottom: 4,
129 | },
130 | headerButton: {
131 | flexDirection: "row",
132 | alignItems: "center",
133 | padding: 12,
134 | gap: 6,
135 | },
136 | headerText: {
137 | fontSize: 15,
138 | },
139 | demoText: {
140 | fontSize: 14,
141 | opacity: 0.7,
142 | },
143 | footer: {
144 | padding: 16,
145 | flexDirection: "row",
146 | alignItems: "center",
147 | justifyContent: "space-between",
148 | gap: 12,
149 | },
150 | image: {
151 | width: 32,
152 | aspectRatio: 1,
153 | borderRadius: 16,
154 | },
155 | footerText: {
156 | fontSize: 18,
157 | },
158 | cluster: {
159 | flexDirection: "row",
160 | alignItems: "center",
161 | gap: 12,
162 | },
163 | contactIcon: {
164 | padding: 4,
165 | },
166 | });
167 |
--------------------------------------------------------------------------------
/app/wa-status/index.ios.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, ScrollView, StyleSheet, Pressable } from "react-native";
2 | import React, { ReactNode } from "react";
3 | import { ThemedText, ThemedTextWrapper } from "@/components/ThemedText";
4 | import { ThemedView } from "@/components/ThemedView";
5 | import { Image } from "expo-image";
6 | import { useThemeColor } from "@/hooks/useThemeColor";
7 | import { Link } from "expo-router";
8 | import { Ellipsis } from "lucide-react-native";
9 | import GlassViewComponent from "@/components/ui/glassy-view";
10 | import {
11 | Button,
12 | ContextMenu,
13 | Host,
14 | Picker,
15 | Label,
16 | Submenu,
17 | Switch,
18 | Spacer,
19 | } from "@expo/ui/swift-ui";
20 |
21 | export default function Index() {
22 | return (
23 | <>
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | >
32 | );
33 | }
34 |
35 | interface ClusterContainerProps {
36 | children: ReactNode;
37 | }
38 |
39 | export const PageContainer = ({ children }: ClusterContainerProps) => {
40 | return (
41 |
45 | {children}
46 |
47 | );
48 | };
49 |
50 | export const ClusterContainer = ({ children }: ClusterContainerProps) => {
51 | return (
52 |
53 | {children}
54 |
55 | );
56 | };
57 |
58 | interface ClusterItemProps {
59 | imgSrc?: string;
60 | views?: number;
61 | time?: string;
62 | linkHref?: string;
63 | }
64 |
65 | export const ClusterItem = ({
66 | imgSrc = "https://wallpapercave.com/wp/wp3161438.jpg",
67 | views = 24,
68 | time = "1h ago",
69 | linkHref = "/wa-status/page",
70 | }: ClusterItemProps) => {
71 | return (
72 |
73 |
74 |
75 |
81 |
82 |
83 |
84 |
85 | {views} view{views !== 1 ? "s" : ""}
86 |
87 |
88 | {time}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
101 |
108 |
109 | More}
111 | >
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | );
128 | };
129 |
130 | const styles = StyleSheet.create({
131 | container: {
132 | flex: 1,
133 | padding: 16,
134 | },
135 | clusterContainer: {
136 | borderRadius: 12,
137 | borderCurve: "continuous",
138 | overflow: "hidden",
139 | },
140 | clusterItem: {
141 | // height: 72,
142 | flexDirection: "row",
143 | },
144 | thumbContainer: {
145 | paddingHorizontal: 16,
146 | paddingVertical: 12,
147 | paddingRight: 12,
148 | },
149 | thumb: {
150 | width: 56,
151 | aspectRatio: 1,
152 | borderRadius: "50%",
153 | },
154 | contentWrapper: {
155 | flex: 1,
156 | borderBottomWidth: 1,
157 | borderColor: "#88888820",
158 | flexDirection: "row",
159 | alignItems: "center",
160 | justifyContent: "space-between",
161 | paddingRight: 16,
162 | },
163 | main: {
164 | justifyContent: "center",
165 | flex: 1,
166 | gap: 2,
167 | },
168 | views: {
169 | fontSize: 17,
170 | },
171 | time: {
172 | opacity: 0.6,
173 | },
174 | });
175 |
--------------------------------------------------------------------------------
/components/RefreshLogo.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, View } from "react-native";
2 | import React, { ComponentProps } from "react";
3 | import Svg, { Path } from "react-native-svg";
4 | import { useThemeColor } from "@/hooks/useThemeColor";
5 | import Animated, {
6 | Extrapolation,
7 | interpolate,
8 | runOnJS,
9 | SharedValue,
10 | useAnimatedProps,
11 | useAnimatedReaction,
12 | useDerivedValue,
13 | } from "react-native-reanimated";
14 | import { svgPathProperties } from "svg-path-properties";
15 | import * as Haptics from "expo-haptics";
16 |
17 | interface RefreshLogoProps {
18 | scrollY: SharedValue;
19 | maxScrollY?: number;
20 | svgProps?: ComponentProps;
21 | pathProps?: ComponentProps;
22 | width?: number;
23 | height?: number;
24 | d?: string;
25 | size?: number;
26 | visiblePercent?: number;
27 | inverted?: boolean;
28 | }
29 |
30 | const AnimatedSvg = Animated.createAnimatedComponent(Svg);
31 | const AnimatedPath = Animated.createAnimatedComponent(Path);
32 |
33 | const PATH_LENGTH_ADJUSTMENT = 0.5; // Adjustment for path length calculation
34 | const MAX_PROGRESS = 0.99; // Maximum progress for the animation
35 | const FADE_THRESHOLD = 0.4; // Threshold for fading effect
36 | const FADE_OPACITY = 0.3; // Opacity when faded
37 | const STROKE_WIDTH = 1.6; // Default stroke width for the path
38 | const VISIBLE_PERCENT = 12; // Default visible percent for the path
39 | const SIZE = 50; // Default size for the logo
40 | const STROKE_CAP = "round"; // Default stroke cap for the path
41 | const MAX_SCROLL_Y = 100; // Default maximum scroll Y value
42 | const WIDTH = 24; // Default width for the SVG
43 | const HEIGHT = 24; // Default height for the SVG
44 |
45 | const DEFAULT_D =
46 | "M22 4.00002C22 4.00002 21.3 6.10002 20 7.40002C21.6 17.4 10.6 24.7 2 19C4.2 19.1 6.4 18.4 8 17C3 15.5 0.5 9.60002 3 5.00002C5.2 7.60002 8.6 9.10002 12 9.00002C11.1 4.80002 16 2.40002 19 5.20002C20.1 5.20002 22 4.00002 22 4.00002Z";
47 |
48 | const progressiveFeedback = () => {
49 | Haptics.selectionAsync();
50 | };
51 |
52 | const completeFeedback = () => {
53 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
54 | };
55 |
56 | export default function RefreshLogo({
57 | scrollY,
58 | maxScrollY = MAX_SCROLL_Y,
59 | svgProps,
60 | pathProps,
61 | width = WIDTH,
62 | height = HEIGHT,
63 | size = SIZE,
64 | d = DEFAULT_D,
65 | visiblePercent = VISIBLE_PERCENT,
66 | inverted = false,
67 | }: RefreshLogoProps) {
68 | const textColor = useThemeColor("text");
69 | const length =
70 | new svgPathProperties(d).getTotalLength() + PATH_LENGTH_ADJUSTMENT;
71 | const viewBox = `0 0 ${width} ${height}`;
72 |
73 | const combinedPathProps = {
74 | ...pathProps,
75 | d,
76 | stroke: pathProps?.stroke || textColor,
77 | strokeWidth: pathProps?.strokeWidth || STROKE_WIDTH,
78 | strokeLinecap: pathProps?.strokeLinecap || STROKE_CAP,
79 | };
80 |
81 | const getPortion = (percent: number) => {
82 | "worklet";
83 | return (percent / 100) * length;
84 | };
85 |
86 | const progress = useDerivedValue(() => {
87 | return Math.min(1, -(scrollY.value / maxScrollY));
88 | });
89 |
90 | useAnimatedReaction(
91 | () => progress.value,
92 | (curr) => {
93 | const p = curr * 100;
94 | const step = 5;
95 | if (p >= step && p % step < 1 && p < 100) {
96 | // runOnJS(progressiveFeedback)(); // Uncomment to enable progressive feedback(feels a bit too much)
97 | } else if (p >= 100) {
98 | runOnJS(completeFeedback)();
99 | }
100 | }
101 | );
102 |
103 | const animatedPathProps = useAnimatedProps(() => {
104 | const visibleLength = getPortion(visiblePercent);
105 | const p = Math.max(
106 | -MAX_PROGRESS,
107 | inverted ? Math.max(0, progress.value) : Math.min(0, -progress.value)
108 | );
109 | const offset = (length + visibleLength) * p;
110 |
111 | return {
112 | strokeDashoffset: offset + visibleLength,
113 | strokeDasharray: `${getPortion(visiblePercent)} ${length}`,
114 | };
115 | });
116 |
117 | const animatedFadedPathProps = useAnimatedProps(() => {
118 | return {
119 | opacity: interpolate(
120 | progress.value,
121 | [0, FADE_THRESHOLD],
122 | [1, FADE_OPACITY],
123 | Extrapolation.CLAMP
124 | ),
125 | };
126 | });
127 |
128 | return (
129 |
130 |
136 |
141 |
146 |
147 |
148 | );
149 | }
150 |
151 | const styles = StyleSheet.create({
152 | container: {
153 | alignItems: "center",
154 | justifyContent: "center",
155 | },
156 | });
157 |
--------------------------------------------------------------------------------
/components/stacked-input/input.tsx:
--------------------------------------------------------------------------------
1 | import { TextInput, StyleSheet, TextInputProps } from "react-native";
2 | import React, { ReactElement, useCallback, useRef } from "react";
3 | import Animated, {
4 | useAnimatedReaction,
5 | useAnimatedStyle,
6 | useDerivedValue,
7 | useSharedValue,
8 | withSpring,
9 | } from "react-native-reanimated";
10 | import { useStackedInput } from "./provider";
11 | import { BlurView } from "expo-blur";
12 | import { scheduleOnRN } from "react-native-worklets";
13 | import { ThemedView } from "../ThemedView";
14 | import { ThemedTextWrapper } from "../ThemedText";
15 |
16 | const TRANSFORM_DISTANCE = -6;
17 | const SCALE_FACTOR = 0.95;
18 | const MAX_VISIBLE = 2;
19 | const NEXT_SCALE = 1.2;
20 | const NEXT_TRANSFORM = -6;
21 |
22 | const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
23 |
24 | const SPRING_CONFIG = {
25 | damping: 10,
26 | stiffness: 80,
27 | mass: 0.6,
28 | };
29 |
30 | const SPRING_CONFIG_INV = {
31 | damping: 10,
32 | stiffness: 80,
33 | mass: 1,
34 | };
35 |
36 | export default function Input({
37 | index,
38 | stackedInputRef,
39 | children,
40 | ...props
41 | }: {
42 | index: number;
43 | stackedInputRef?: React.RefObject;
44 | children?: ReactElement;
45 | } & TextInputProps) {
46 | const { currentIndex, itemStyles, itemProps } = useStackedInput();
47 | const _inputRef = useRef(null);
48 | const inputRef = stackedInputRef || _inputRef;
49 | const previousIndex = useSharedValue(0);
50 |
51 | const applySpring = (value: number) => {
52 | "worklet";
53 | const isGoingForward = currentIndex.value > previousIndex.value;
54 |
55 | const CONFIG = isGoingForward ? SPRING_CONFIG : SPRING_CONFIG_INV;
56 | return withSpring(value, CONFIG);
57 | };
58 |
59 | const intensity = useDerivedValue(() => {
60 | const isNext = index > currentIndex.value;
61 | return applySpring(isNext ? 20 : 0);
62 | });
63 |
64 | const focusInput = () => {
65 | inputRef.current?.focus();
66 | };
67 |
68 | useAnimatedReaction(
69 | () => currentIndex.value,
70 | (current, prev) => {
71 | const isCurrent = current === index;
72 |
73 | previousIndex.value = prev || 0;
74 |
75 | if (isCurrent) {
76 | scheduleOnRN(focusInput);
77 | }
78 | }
79 | );
80 |
81 | const animatedStyle = useAnimatedStyle(() => {
82 | const isCurrent = index === currentIndex.value;
83 | const isPrevious = index < currentIndex.value;
84 | const isNext = index > currentIndex.value;
85 | const distance = currentIndex.value - index;
86 | const factor = Math.max(-MAX_VISIBLE, Math.min(MAX_VISIBLE, distance));
87 |
88 | return {
89 | transform: [
90 | {
91 | translateY: applySpring(
92 | (isNext ? NEXT_TRANSFORM : factor) * TRANSFORM_DISTANCE
93 | ),
94 | },
95 | {
96 | scale: applySpring(
97 | isCurrent
98 | ? 1
99 | : isNext
100 | ? NEXT_SCALE
101 | : SCALE_FACTOR ** Math.abs(factor)
102 | ),
103 | },
104 | ],
105 | pointerEvents: isCurrent ? "auto" : "box-only",
106 | opacity: applySpring(isNext || Math.abs(factor) > MAX_VISIBLE ? 0 : 1),
107 | };
108 | });
109 |
110 | const blurAnimatedStyle = useAnimatedStyle(() => {
111 | const isNext = index > currentIndex.value;
112 | return { display: isNext ? "flex" : "none" };
113 | });
114 |
115 | const onFocus = useCallback((e: any) => {
116 | props.onFocus?.(e);
117 | currentIndex.value = index;
118 | }, []);
119 |
120 | return (
121 |
122 |
123 |
124 |
136 |
137 | {children}
138 |
139 |
144 |
145 | );
146 | }
147 |
148 | const styles = StyleSheet.create({
149 | block: {
150 | borderWidth: 1,
151 | borderColor: "#88888899",
152 | width: "100%",
153 | height: "100%",
154 | overflow: "hidden",
155 | },
156 | text: {
157 | padding: 14,
158 | fontSize: 17,
159 | flex: 1,
160 | },
161 | hiddenText: {
162 | opacity: 0,
163 | position: "absolute",
164 | pointerEvents: "none",
165 | },
166 | rounded: {
167 | borderRadius: 14,
168 | borderCurve: "continuous",
169 | },
170 | container: {
171 | right: 0,
172 | left: 0,
173 | position: "absolute",
174 | height: "100%",
175 | },
176 | });
177 |
--------------------------------------------------------------------------------
/components/ui/check-box.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Pressable, StyleProp, StyleSheet, ViewStyle } from "react-native";
3 | import Animated, {
4 | useSharedValue,
5 | useAnimatedStyle,
6 | withTiming,
7 | interpolateColor,
8 | withSpring,
9 | SharedValue,
10 | useAnimatedReaction,
11 | useDerivedValue,
12 | useAnimatedProps,
13 | } from "react-native-reanimated";
14 | import { scheduleOnRN } from "react-native-worklets";
15 | import { Entypo } from "@expo/vector-icons";
16 |
17 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
18 |
19 | const ANIMATION_DURATION = 200;
20 | const SPRING_SCALE = 0.8;
21 | const DEFAULT_COLORS = {
22 | checked: "#4F46E5",
23 | initial: "#D1D5DB",
24 | disabled: "#9CA3AF",
25 | };
26 |
27 | const interpolateDisabledColor = (
28 | disabledValue: number,
29 | activeColor: string,
30 | disabledColor: string
31 | ) => {
32 | "worklet";
33 | return interpolateColor(disabledValue, [0, 1], [activeColor, disabledColor]);
34 | };
35 |
36 | interface CheckBoxProps {
37 | checkedColor?: string;
38 | initialColor?: string;
39 | disabledColor?: string;
40 | checked?: SharedValue;
41 | onChange?: (checked: boolean) => void;
42 | size?: number;
43 | style?: StyleProp;
44 | disabled?: SharedValue;
45 | }
46 |
47 | export const CheckBox: React.FC = ({
48 | checkedColor = DEFAULT_COLORS.checked,
49 | initialColor = DEFAULT_COLORS.initial,
50 | disabledColor = DEFAULT_COLORS.disabled,
51 | checked,
52 | onChange,
53 | size = 24,
54 | style,
55 | disabled,
56 | }) => {
57 | const checkedSV = useSharedValue(0);
58 | const scale = useSharedValue(1);
59 | const disabledSV = useSharedValue(0);
60 |
61 | const handleAnimatedReaction = (
62 | sharedValue: SharedValue,
63 | value: boolean
64 | ) => {
65 | "worklet";
66 | sharedValue.value = withTiming(value ? 1 : 0, {
67 | duration: ANIMATION_DURATION,
68 | });
69 | };
70 |
71 | useAnimatedReaction(
72 | () => checked?.value ?? false,
73 | (current, previous) => {
74 | if (current !== previous) {
75 | handleAnimatedReaction(checkedSV, current);
76 | }
77 | }
78 | );
79 |
80 | useAnimatedReaction(
81 | () => disabled?.value ?? false,
82 | (current, previous) => {
83 | if (current !== previous) {
84 | handleAnimatedReaction(disabledSV, current);
85 | }
86 | }
87 | );
88 |
89 | const animatedStyle = useAnimatedStyle(() => {
90 | const colorFrom = interpolateDisabledColor(
91 | disabledSV.value,
92 | initialColor,
93 | disabledColor
94 | );
95 | const colorTo = interpolateDisabledColor(
96 | disabledSV.value,
97 | checkedColor,
98 | disabledColor
99 | );
100 |
101 | return {
102 | transform: [{ scale: scale.value }],
103 | backgroundColor: interpolateColor(
104 | checkedSV.value,
105 | [0, 1],
106 | [colorFrom, colorTo]
107 | ),
108 | };
109 | });
110 |
111 | const iconAnimatedStyle = useAnimatedStyle(() => ({
112 | opacity: checkedSV.value,
113 | }));
114 |
115 | const isDisabled = useDerivedValue(() => disabledSV.value === 1);
116 |
117 | const handlePress = () => {
118 | if (isDisabled.value) return;
119 |
120 | if (!checked) {
121 | checkedSV.value = withTiming(checkedSV.value ? 0 : 1, {
122 | duration: ANIMATION_DURATION,
123 | });
124 | if (onChange) scheduleOnRN(() => onChange(!checkedSV.value));
125 | } else {
126 | if (onChange) scheduleOnRN(() => onChange(!checked.value));
127 | }
128 | };
129 |
130 | const handlePressIn = () => {
131 | if (isDisabled.value) return;
132 | scale.value = withSpring(SPRING_SCALE);
133 | };
134 |
135 | const handlePressOut = () => {
136 | if (isDisabled.value) return;
137 | scale.value = withSpring(1);
138 | };
139 |
140 | const pressableAnimatedProps = useAnimatedProps(() => ({
141 | disabled: isDisabled.value,
142 | }));
143 |
144 | return (
145 |
151 |
163 |
167 |
168 |
169 |
170 |
171 | );
172 | };
173 |
174 | const styles = StyleSheet.create({
175 | box: {
176 | alignItems: "center",
177 | justifyContent: "center",
178 | overflow: "hidden",
179 | borderRadius: 6,
180 | borderCurve: "continuous",
181 | },
182 | centered: {
183 | alignItems: "center",
184 | justifyContent: "center",
185 | },
186 | });
187 |
188 | export default CheckBox;
189 |
--------------------------------------------------------------------------------
/components/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | View,
3 | TextInput,
4 | StyleSheet,
5 | Platform,
6 | StyleProp,
7 | ViewStyle,
8 | TextInputProps,
9 | } from "react-native";
10 | import React, { useEffect, useRef } from "react";
11 | import Animated, {
12 | Easing,
13 | measure,
14 | useAnimatedRef,
15 | useAnimatedStyle,
16 | useSharedValue,
17 | withTiming,
18 | WithTimingConfig,
19 | } from "react-native-reanimated";
20 | import { Gesture, GestureDetector } from "react-native-gesture-handler";
21 | import { scheduleOnUI } from "react-native-worklets";
22 |
23 | const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
24 |
25 | const isWeb = Platform;
26 | const HANDLE_WIDTH = 20;
27 | const HANDLE_HEIGHT = 10;
28 | const MIN_SIZE = 40;
29 | const LINE_HEIGHT_DEFAULT = 18;
30 | const PADDING_DEFAULT = 8;
31 |
32 | type TextAreaProps = {
33 | containerStyle?: StyleProp;
34 | maxHeight?: number;
35 | minHeight?: number;
36 | maxWidth?: number;
37 | minWidth?: number;
38 | autoAdjustHeight?: boolean;
39 | lineHeight?: number;
40 | padding?: number;
41 | borderWidth?: number;
42 | timingConfig?: WithTimingConfig;
43 | };
44 |
45 | const TIMING_CONFIG: WithTimingConfig = {
46 | duration: 100,
47 | easing: Easing.inOut(Easing.ease),
48 | };
49 |
50 | export default function TextArea({
51 | style,
52 | containerStyle,
53 | maxHeight = 200,
54 | minHeight = MIN_SIZE,
55 | maxWidth,
56 | minWidth = MIN_SIZE,
57 | autoAdjustHeight = true,
58 | lineHeight = LINE_HEIGHT_DEFAULT,
59 | padding = PADDING_DEFAULT,
60 | borderWidth = 0,
61 | value,
62 | onChangeText,
63 | timingConfig = TIMING_CONFIG,
64 | ...props
65 | }: TextAreaProps & TextInputProps) {
66 | const textContainerRef = useAnimatedRef();
67 | const cordX = useSharedValue(0);
68 | const cordY = useSharedValue(0);
69 | const layout = useSharedValue({ width: 0, height: 0 });
70 | const textInputRef = useRef(null);
71 |
72 | const chooseMax = (val: number, axis: "x" | "y") => {
73 | "worklet";
74 | const key = axis === "x" ? "width" : "height";
75 | const base = layout.value[key] + val;
76 | let min = MIN_SIZE;
77 | let max = Number.POSITIVE_INFINITY;
78 | if (axis === "x") {
79 | if (typeof minWidth === "number") min = minWidth;
80 | if (typeof maxWidth === "number") max = maxWidth;
81 | } else {
82 | if (typeof minHeight === "number") min = minHeight;
83 | if (typeof maxHeight === "number") max = maxHeight;
84 | }
85 | return Math.max(min, Math.min(base, max));
86 | };
87 |
88 | const applyConfig = (value: number) => {
89 | "worklet";
90 | return autoAdjustHeight ? withTiming(value, timingConfig) : value;
91 | };
92 |
93 | const resizePan = Gesture.Pan()
94 | .minDistance(isWeb ? 1 : 0)
95 | .onUpdate((e) => {
96 | cordX.value = e.translationX;
97 | cordY.value = e.translationY;
98 | })
99 | .onEnd(() => {
100 | layout.value = {
101 | width: chooseMax(cordX.value, "x"),
102 | height: chooseMax(cordY.value, "y"),
103 | };
104 | cordX.value = 0;
105 | cordY.value = 0;
106 | });
107 |
108 | const animatedStyle = useAnimatedStyle(() => {
109 | if (layout.value.width === 0 || layout.value.height === 0) return {};
110 |
111 | return {
112 | width: layout.value.width + cordX.value,
113 | height: applyConfig(
114 | Math.min(maxHeight, layout.value.height + cordY.value)
115 | ),
116 | };
117 | });
118 |
119 | const handleAutoAdjustHeight = (text: string) => {
120 | "worklet";
121 | if (!autoAdjustHeight) return;
122 | const lines = text.split("\n").length;
123 |
124 | const newHeight = Math.max(
125 | lines * lineHeight + padding * 2 + borderWidth * 2,
126 | minHeight
127 | );
128 | layout.value = {
129 | ...layout.value,
130 | height: newHeight,
131 | };
132 | };
133 |
134 | useEffect(() => {
135 | scheduleOnUI(() => {
136 | const measurement = measure(textContainerRef);
137 | if (!measurement) return;
138 | layout.value = {
139 | width: measurement.width,
140 | height: measurement.height,
141 | };
142 | handleAutoAdjustHeight(value || "");
143 | });
144 | }, []);
145 |
146 | const maxStyle = {
147 | maxHeight: maxHeight,
148 | minHeight: minHeight,
149 | maxWidth: maxWidth,
150 | minWidth: minWidth,
151 | };
152 |
153 | return (
154 |
158 | {
174 | scheduleOnUI(() => {
175 | handleAutoAdjustHeight(text);
176 | });
177 | onChangeText?.(text);
178 | }}
179 | {...props}
180 | />
181 | {!autoAdjustHeight && (
182 |
183 |
184 |
185 | )}
186 |
187 | );
188 | }
189 |
190 | const styles = StyleSheet.create({
191 | handle: {
192 | width: HANDLE_WIDTH,
193 | height: HANDLE_HEIGHT,
194 | borderRadius: 2.5,
195 | backgroundColor: "red",
196 | position: "absolute",
197 | right: 4,
198 | bottom: 4,
199 | cursor: "se-resize" as any,
200 | },
201 | });
202 |
--------------------------------------------------------------------------------
/components/untitled/bottom-bar.tsx:
--------------------------------------------------------------------------------
1 | import { Platform, StyleSheet, View } from "react-native";
2 | import React from "react";
3 | import { useSafeAreaInsets } from "react-native-safe-area-context";
4 | import { useScreenAnimation } from "react-native-screen-transitions";
5 | import Animated, {
6 | interpolate,
7 | useAnimatedStyle,
8 | } from "react-native-reanimated";
9 | import Feather from "@expo/vector-icons/Feather";
10 | import { ThemedView, ThemedViewWrapper } from "../ThemedView";
11 | import Ionicons from "@expo/vector-icons/Ionicons";
12 | import { Colors } from "@/constants/Colors";
13 | import { useThemeColor } from "@/hooks/useThemeColor";
14 | import { ThemedText } from "../ThemedText";
15 | import PressableBounce from "../PresableBounce";
16 |
17 | const SPACING = 14;
18 | const GAP = 10;
19 | const HEIGHT = 58;
20 |
21 | const DATA = {
22 | title: "Vikings - 2013",
23 | name: "untitled project",
24 | };
25 |
26 | export interface UntitledBottomBarProps {
27 | type?: "fill" | "collapse";
28 | }
29 |
30 | export default function UntitledBottomBar({
31 | type = "collapse",
32 | }: UntitledBottomBarProps) {
33 | const { bottom } = useSafeAreaInsets();
34 | const props = useScreenAnimation();
35 | const bg = useThemeColor("background");
36 |
37 | if (Platform.OS === "web") {
38 | return;
39 | }
40 |
41 | const headerAnimatedStyle = useAnimatedStyle(() => {
42 | const {
43 | progress,
44 |
45 | layouts: {
46 | screen: { width },
47 | },
48 | } = props.value;
49 |
50 | return {
51 | transform: [
52 | {
53 | translateX: interpolate(progress, [0, 1, 2], [-width, 0, width]),
54 | },
55 | ],
56 | opacity: interpolate(progress, [0, 0.1, 1, 1.9, 2], [0, 1, 1, 1, 0]),
57 | };
58 | });
59 |
60 | const controlAnimatedStyle = useAnimatedStyle(() => {
61 | const {
62 | progress,
63 | layouts: {
64 | screen: { width },
65 | },
66 | } = props.value;
67 | const FULL_WIDTH = width - SPACING * 2;
68 | const COLLAPSED_WIDTH = FULL_WIDTH - HEIGHT - SPACING;
69 |
70 | return {
71 | width: interpolate(
72 | progress,
73 | [0, 1, 2],
74 | type === "fill"
75 | ? [COLLAPSED_WIDTH, FULL_WIDTH, FULL_WIDTH]
76 | : [FULL_WIDTH, COLLAPSED_WIDTH, FULL_WIDTH]
77 | ),
78 | };
79 | });
80 |
81 | const buttonAnimatedStyle = useAnimatedStyle(() => {
82 | const { progress } = props.value;
83 |
84 | return {
85 | transform: [
86 | {
87 | translateX:
88 | type === "collapse"
89 | ? interpolate(progress, [0, 1, 2], [0, 0, GAP])
90 | : 0,
91 | },
92 | ],
93 | };
94 | });
95 |
96 | return (
97 |
106 |
107 |
108 |
112 |
113 |
114 |
115 |
116 |
117 | {DATA.title}
118 |
119 |
120 | {DATA.name}
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | {type === "collapse" && (
130 |
131 |
135 |
136 |
137 |
138 |
139 |
140 | )}
141 |
142 | );
143 | }
144 |
145 | const styles = StyleSheet.create({
146 | container: {
147 | height: HEIGHT,
148 | left: 0,
149 | right: 0,
150 | position: "absolute",
151 | gap: GAP,
152 | flexDirection: "row",
153 | margin: SPACING,
154 | zIndex: 100,
155 | // backgroundColor: "red",
156 | },
157 | controlBar: {
158 | height: "100%",
159 | borderRadius: 50,
160 | borderCurve: "continuous",
161 | padding: 5,
162 | flexDirection: "row",
163 | alignItems: "center",
164 | gap: 8,
165 | },
166 | addButton: {
167 | height: HEIGHT,
168 | aspectRatio: 1,
169 | alignItems: "center",
170 | justifyContent: "center",
171 | borderRadius: "50%",
172 | },
173 | shadow: {
174 | boxShadow: "0px 0px 32px rgba(0,0,0,0.1)",
175 | },
176 | disk: {
177 | experimental_backgroundImage: `linear-gradient(135deg, ${Colors.dark.untitledGradient2} 0%, ${Colors.dark.untitledGradient1} 80%)`,
178 | height: "100%",
179 | aspectRatio: 1,
180 | borderRadius: "50%",
181 | alignItems: "center",
182 | justifyContent: "center",
183 | borderWidth: 0.8,
184 | },
185 | content: {
186 | flex: 1,
187 | justifyContent: "center",
188 | gap: 1,
189 | },
190 | title: {
191 | fontSize: 15,
192 | color: "white",
193 | opacity: 0.8,
194 | letterSpacing: -0.25,
195 | },
196 | subtitle: {
197 | fontSize: 12.2,
198 | opacity: 0.4,
199 | color: "white",
200 | },
201 | share: {
202 | height: "100%",
203 | aspectRatio: 1,
204 | alignItems: "center",
205 | justifyContent: "center",
206 | borderRadius: "50%",
207 | },
208 | });
209 |
--------------------------------------------------------------------------------
/components/drawer/DrawerItem.tsx:
--------------------------------------------------------------------------------
1 | import { PlatformPressable, Text } from "@react-navigation/elements";
2 | import { type Route, useTheme } from "@react-navigation/native";
3 | import * as React from "react";
4 | import {
5 | type StyleProp,
6 | StyleSheet,
7 | type TextStyle,
8 | View,
9 | type ViewStyle,
10 | } from "react-native";
11 | import { Href, Link } from "expo-router";
12 | import { useThemeColor } from "@/hooks/useThemeColor";
13 |
14 | type Props = {
15 | /**
16 | * The route object which should be specified by the drawer item.
17 | */
18 | route?: Route;
19 | /**
20 | * The `href` to use for the anchor tag on web
21 | */
22 | href: Href;
23 | /**
24 | * The label text of the item.
25 | */
26 | label:
27 | | string
28 | | ((props: { focused: boolean; color: string }) => React.ReactNode);
29 | /**
30 | * Icon to display for the `DrawerItem`.
31 | */
32 | icon?: (props: {
33 | focused: boolean;
34 | size: number;
35 | color: string;
36 | }) => React.ReactNode;
37 | /**
38 | * Whether to highlight the drawer item as active.
39 | */
40 | focused?: boolean;
41 | /**
42 | * Function to execute on press.
43 | */
44 | onPress: () => void;
45 | /**
46 | * Color for the icon and label when the item is active.
47 | */
48 | activeTintColor?: string;
49 | /**
50 | * Color for the icon and label when the item is inactive.
51 | */
52 | inactiveTintColor?: string;
53 | /**
54 | * Background color for item when its active.
55 | */
56 | activeBackgroundColor?: string;
57 | /**
58 | * Background color for item when its inactive.
59 | */
60 | inactiveBackgroundColor?: string;
61 | /**
62 | * Color of the touchable effect on press.
63 | * Only supported on Android.
64 | *
65 | * @platform android
66 | */
67 | pressColor?: string;
68 | /**
69 | * Opacity of the touchable effect on press.
70 | * Only supported on iOS.
71 | *
72 | * @platform ios
73 | */
74 | pressOpacity?: number;
75 | /**
76 | * Style object for the label element.
77 | */
78 | labelStyle?: StyleProp;
79 | /**
80 | * Style object for the wrapper element.
81 | */
82 | style?: StyleProp;
83 | /**
84 | * Whether label font should scale to respect Text Size accessibility settings.
85 | */
86 | allowFontScaling?: boolean;
87 |
88 | /**
89 | * Accessibility label for drawer item.
90 | */
91 | accessibilityLabel?: string;
92 | /**
93 | * ID to locate this drawer item in tests.
94 | */
95 | testID?: string;
96 | };
97 |
98 | /**
99 | * A component used to show an action item with an icon and a label in a navigation drawer.
100 | */
101 | export function DrawerItem(props: Props) {
102 | const { colors, fonts } = useTheme();
103 |
104 | const {
105 | href,
106 | icon,
107 | label,
108 | labelStyle,
109 | focused = false,
110 | allowFontScaling,
111 | activeTintColor = colors.primary,
112 | inactiveTintColor = "",
113 | activeBackgroundColor = "",
114 | inactiveBackgroundColor = "transparent",
115 | style,
116 | onPress,
117 | pressColor,
118 | pressOpacity = 1,
119 | testID,
120 | accessibilityLabel,
121 | route,
122 | ...rest
123 | } = props;
124 |
125 | const { borderRadius = 56 } = StyleSheet.flatten(style || {});
126 | const color = focused ? activeTintColor : inactiveTintColor;
127 | const backgroundColor = focused
128 | ? activeBackgroundColor
129 | : inactiveBackgroundColor;
130 |
131 | const iconNode = icon ? icon({ size: 24, focused, color }) : null;
132 | const noPreview = (route?.params as any)?.noPreview || false;
133 | const bg = useThemeColor("barColor");
134 | return (
135 |
140 |
141 | {!noPreview && }
142 |
143 | {}}
147 | />
148 | {}}
153 | />
154 |
155 |
156 |
157 |
168 |
169 | {iconNode}
170 |
171 | {typeof label === "string" ? (
172 |
182 | {label}
183 |
184 | ) : (
185 | label({ color, focused })
186 | )}
187 |
188 |
189 |
190 |
191 |
192 |
193 | );
194 | }
195 |
196 | const styles = StyleSheet.create({
197 | container: {
198 | overflow: "hidden",
199 | },
200 | wrapper: {
201 | flexDirection: "row",
202 | alignItems: "center",
203 | paddingVertical: 11,
204 | paddingStart: 16,
205 | paddingEnd: 24,
206 | },
207 | label: {
208 | marginEnd: 12,
209 | marginVertical: 4,
210 | flex: 1,
211 | },
212 | labelText: {
213 | lineHeight: 24,
214 | textAlignVertical: "center",
215 | },
216 | });
217 |
--------------------------------------------------------------------------------
/components/Slider.tsx:
--------------------------------------------------------------------------------
1 | import { Feedback } from "@/functions";
2 | import React from "react";
3 | import { Platform, StyleSheet } from "react-native";
4 | import { Gesture, GestureDetector } from "react-native-gesture-handler";
5 | import Animated, {
6 | SharedValue,
7 | useAnimatedStyle,
8 | useSharedValue,
9 | interpolate,
10 | withSpring,
11 | withTiming,
12 | withDecay,
13 | useAnimatedReaction,
14 | measure,
15 | useAnimatedRef,
16 | Extrapolation,
17 | } from "react-native-reanimated";
18 | import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
19 |
20 | type SliderProps = {
21 | value?: SharedValue;
22 | max?: number;
23 | trackColor?: string;
24 | thumbColor?: string;
25 | pressed?: SharedValue;
26 | scrubbing?: SharedValue;
27 | };
28 |
29 | const isWeb = Platform.OS === "web";
30 |
31 | const TRACK_HEIGHT = 8;
32 | const EXPANDED_TRACK_HEIGHT = 14;
33 | const VELOCITY_RESISTANCE = isWeb ? 0.2 : 0.8;
34 | const MAX_OVERDRAG = 12;
35 | const OVERDRAG_RESISTANCE = 0.1;
36 |
37 | export default function Slider({
38 | value: externalValue,
39 | max = 100,
40 | thumbColor = "#007AFF",
41 | trackColor = "#CCCCCC",
42 | pressed: externalPressed,
43 | scrubbing: externalScrubbing,
44 | }: SliderProps) {
45 | const value = externalValue || useSharedValue(0);
46 | const startValue = useSharedValue(0);
47 | const pressed = externalPressed || useSharedValue(false);
48 | const overDrag = useSharedValue(0);
49 | const trackRef = useAnimatedRef();
50 | const sliderWidth = useSharedValue(0);
51 | const scrubbing = externalScrubbing || useSharedValue(false);
52 |
53 | const HapticFeedback = () => {
54 | Feedback.selection();
55 | };
56 |
57 | useAnimatedReaction(
58 | () => {
59 | const atEdge = value.value === 0 || value.value === max;
60 | const atMaxOverDrag = Math.abs(overDrag.value) >= MAX_OVERDRAG;
61 | return { atEdge, atMaxOverDrag };
62 | },
63 | (current, previous) => {
64 | if (
65 | (current.atEdge && !previous?.atEdge) ||
66 | (current.atMaxOverDrag && !previous?.atMaxOverDrag)
67 | ) {
68 | scheduleOnRN(HapticFeedback);
69 | }
70 | }
71 | );
72 |
73 | const panGesture = Gesture.Pan()
74 | .onTouchesDown(() => {
75 | pressed.value = true;
76 | scrubbing.value = true;
77 | })
78 | .onTouchesUp(() => {
79 | pressed.value = false;
80 | })
81 | .onTouchesCancelled(() => {
82 | pressed.value = false;
83 | scrubbing.value = false;
84 | })
85 | .onStart(() => {
86 | startValue.value = value.value;
87 | })
88 | .onUpdate((e) => {
89 | const fraction = e.translationX / sliderWidth.value;
90 | let newValue = startValue.value + fraction * max;
91 |
92 | if (newValue < 0 || newValue > max) {
93 | const overflowFraction =
94 | newValue < 0 ? newValue / max : (newValue - max) / max;
95 |
96 | const px = overflowFraction * sliderWidth.value;
97 |
98 | overDrag.value = Math.max(
99 | -MAX_OVERDRAG,
100 | Math.min(px * OVERDRAG_RESISTANCE, MAX_OVERDRAG)
101 | );
102 | } else {
103 | overDrag.value = 0;
104 | }
105 |
106 | newValue = Math.max(0, Math.min(max, newValue));
107 | value.value = newValue;
108 | })
109 | .onEnd((e) => {
110 | const normalizedVelocity = (e.velocityX / sliderWidth.value) * max;
111 |
112 | if (value.value > 0 && value.value < max) {
113 | value.value = withDecay(
114 | {
115 | velocity: normalizedVelocity * VELOCITY_RESISTANCE,
116 | deceleration: 0.995,
117 | clamp: [0, max],
118 | },
119 | () => {
120 | scrubbing.value = false;
121 | }
122 | );
123 | }
124 |
125 | overDrag.value = withSpring(0);
126 | })
127 | .onFinalize(() => {
128 | pressed.value = false;
129 | });
130 |
131 | const animatedStyle = useAnimatedStyle(() => {
132 | return {
133 | width: interpolate(
134 | value.value,
135 | [0, max],
136 | [0, sliderWidth.value],
137 | Extrapolation.CLAMP
138 | ),
139 | height: EXPANDED_TRACK_HEIGHT,
140 | };
141 | });
142 |
143 | const animatedHeightStyle = useAnimatedStyle(() => {
144 | const clampedOverDrag = overDrag.value;
145 | const absDrag = Math.abs(clampedOverDrag);
146 |
147 | return {
148 | height: withTiming(pressed.value ? EXPANDED_TRACK_HEIGHT : TRACK_HEIGHT, {
149 | duration: 200,
150 | }),
151 | transform: [
152 | {
153 | translateX:
154 | clampedOverDrag / 2 +
155 | interpolate(
156 | absDrag,
157 | [0, MAX_OVERDRAG],
158 | [0, Math.sign(overDrag.value) * (MAX_OVERDRAG / 4)]
159 | ),
160 | },
161 | {
162 | scaleX: 1 + absDrag / sliderWidth.value,
163 | },
164 | {
165 | scaleY:
166 | 1 -
167 | Math.min(
168 | 0.15,
169 | Math.abs(overDrag.value) / (EXPANDED_TRACK_HEIGHT * 4)
170 | ),
171 | },
172 | ],
173 | };
174 | });
175 |
176 | const measureSlider = () => {
177 | scheduleOnUI(() => {
178 | const m = measure(trackRef);
179 | if (m) {
180 | sliderWidth.value = m.width;
181 | }
182 | });
183 | };
184 |
185 | return (
186 |
187 |
196 |
205 |
214 |
215 |
216 |
217 | );
218 | }
219 |
220 | const styles = StyleSheet.create({
221 | track: {
222 | width: "100%",
223 | borderRadius: EXPANDED_TRACK_HEIGHT / 2,
224 | backgroundColor: "#eee",
225 | overflow: "hidden",
226 | borderCurve: "continuous",
227 | },
228 | });
229 |
--------------------------------------------------------------------------------
/components/safari-bar/bar.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Text,
3 | StyleSheet,
4 | Pressable,
5 | ViewStyle,
6 | StyleProp,
7 | useWindowDimensions,
8 | } from "react-native";
9 | import React from "react";
10 | import Animated, {
11 | Extrapolation,
12 | interpolate,
13 | SharedValue,
14 | useAnimatedReaction,
15 | useAnimatedStyle,
16 | useDerivedValue,
17 | useSharedValue,
18 | withTiming,
19 | } from "react-native-reanimated";
20 | import ProgressiveFade from "../ProgressiveFade";
21 | import { useSafeAreaInsets } from "react-native-safe-area-context";
22 | import { GlassContainer, GlassView } from "expo-glass-effect";
23 | import Ionicons from "@expo/vector-icons/Ionicons";
24 | import { ThemedText, ThemedTextWrapper } from "../ThemedText";
25 | import Entypo from "@expo/vector-icons/Entypo";
26 |
27 | const AnimatedGlassView = Animated.createAnimatedComponent(GlassView);
28 |
29 | const BUTTON_SIZE = 50;
30 | const GAP = 6;
31 | const SPACING = 24;
32 | const SCROLL_THRESHOLD = 30;
33 | const SENSITIVITY = 0.01;
34 | const BAR_COLLAPSED_HEIGHT = 40;
35 | const SNAP_THRESHOLD = 0.2;
36 |
37 | export default function Bar({
38 | scrollY,
39 | isScrollEnd,
40 | }: {
41 | scrollY: SharedValue;
42 | isScrollEnd: SharedValue;
43 | }) {
44 | const { bottom } = useSafeAreaInsets();
45 | const isExpanded = useSharedValue(true);
46 | const prevScrollY = useSharedValue(0);
47 | const lastScrollY = useSharedValue(0);
48 | const progress = useSharedValue(1);
49 |
50 | useAnimatedReaction(
51 | () => isScrollEnd.value,
52 | (ended) => {
53 | if (ended) {
54 | lastScrollY.value = scrollY.value;
55 | progress.value = withTiming(progress.value > 0.5 ? 1 : 0, {
56 | duration: 300,
57 | });
58 | }
59 | }
60 | );
61 |
62 | const derivedProgress = useDerivedValue(() => {
63 | return progress.value;
64 | });
65 |
66 | useAnimatedReaction(
67 | () => scrollY.value,
68 | (currentScrollY) => {
69 | // Determine scroll direction
70 | const isScrollingDown = currentScrollY > prevScrollY.value;
71 |
72 | // Update expanded state based on direction
73 | if (Math.abs(prevScrollY.value - currentScrollY) > SCROLL_THRESHOLD) {
74 | isExpanded.value = !isScrollingDown;
75 | }
76 |
77 | // Calculate real-time progress based on scroll velocity
78 | const scrollDelta = prevScrollY.value - currentScrollY;
79 |
80 | // Update progress directly based on scroll movement
81 | const newProgress = Math.max(
82 | 0,
83 | Math.min(1, progress.value + scrollDelta * SENSITIVITY)
84 | );
85 |
86 | progress.value = newProgress;
87 |
88 | // Update previous scroll position for next frame
89 | prevScrollY.value = currentScrollY;
90 | }
91 | );
92 |
93 | const animatedStyle = useAnimatedStyle(() => {
94 | // console.log(isExpanded.value, scrollY.value, prevScrollY.value);
95 | console.log(progress.value, "exp", isExpanded.value);
96 |
97 | return {
98 | transform: [
99 | {
100 | scale: interpolate(
101 | derivedProgress.value,
102 | [0, 1],
103 | [0.8, 1],
104 | Extrapolation.CLAMP
105 | ),
106 | },
107 | {
108 | translateY: interpolate(
109 | derivedProgress.value,
110 | [0, 1],
111 | [30, 0],
112 | Extrapolation.CLAMP
113 | ),
114 | },
115 | ],
116 | };
117 | });
118 |
119 | return (
120 | <>
121 |
122 |
123 |
124 |
125 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | >
138 | );
139 | }
140 |
141 | const AddressBar = ({ progress }: { progress: SharedValue }) => {
142 | const { width } = useWindowDimensions();
143 | const CALCULATED_SPACE = 2 * (BUTTON_SIZE + GAP + SPACING);
144 | const BAR_WIDE_WIDTH = width - CALCULATED_SPACE;
145 | const BAR_COLLAPSED_WIDTH = BAR_WIDE_WIDTH * 0.8;
146 |
147 | const barAnimatedStyle = useAnimatedStyle(() => {
148 | return {
149 | width: interpolate(
150 | progress.value,
151 | [0, 1],
152 | [BAR_COLLAPSED_WIDTH, BAR_WIDE_WIDTH],
153 | Extrapolation.CLAMP
154 | ),
155 | height: interpolate(
156 | progress.value,
157 | [0, 1],
158 | [BAR_COLLAPSED_HEIGHT, BUTTON_SIZE],
159 | Extrapolation.CLAMP
160 | ),
161 | };
162 | });
163 | return (
164 |
165 | Bar
166 |
167 | );
168 | };
169 |
170 | const GlassButton = ({
171 | children,
172 | wrapperStyle,
173 | buttonStyle,
174 | }: {
175 | children: React.ReactElement;
176 | wrapperStyle?: StyleProp;
177 | buttonStyle?: StyleProp;
178 | }) => {
179 | return (
180 |
185 | {children}
186 |
187 | );
188 | };
189 |
190 | const IconButton = ({ children }: { children: React.ReactElement }) => {
191 | return (
192 |
193 | {children}
194 |
195 | );
196 | };
197 |
198 | const styles = StyleSheet.create({
199 | container: {
200 | position: "absolute",
201 | bottom: 16,
202 | right: 0,
203 | left: 0,
204 | // backgroundColor: "red",
205 | },
206 | glassContainer: {
207 | flexDirection: "row",
208 | alignItems: "center",
209 | justifyContent: "center",
210 | gap: GAP,
211 | },
212 | glassWrapper: {
213 | width: BUTTON_SIZE,
214 | height: BUTTON_SIZE,
215 | borderRadius: BUTTON_SIZE / 2,
216 | },
217 | button: {
218 | width: "100%",
219 | height: "100%",
220 | justifyContent: "center",
221 | alignItems: "center",
222 | },
223 | bar: {
224 | // width: 300,
225 | },
226 | });
227 |
--------------------------------------------------------------------------------
/app/3d-rotate/default.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet, TextInput } from "react-native";
2 | import React, { useRef } from "react";
3 | import Rotate3d, { Rotate3dHandle, Rotate3dProps } from "@/components/3dRotate";
4 | import { ThemedText, ThemedTextWrapper } from "@/components/ThemedText";
5 | import PressableBounce from "@/components/PresableBounce";
6 | import { useSharedValue } from "react-native-reanimated";
7 | import { useThemeColor } from "@/hooks/useThemeColor";
8 | import { ArrowLeft, ArrowRight, RotateCcw } from "lucide-react-native";
9 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
10 | import { ThemedViewWrapper } from "@/components/ThemedView";
11 | import DrawPad, { DrawPadHandle } from "expo-drawpad";
12 |
13 | export default function Index({
14 | children,
15 | ...rotate3dProps
16 | }: {
17 | children?: React.ReactNode;
18 | } & Rotate3dProps) {
19 | const rotateRef = useRef(null);
20 |
21 | return (
22 |
28 | rotateRef.current?.flipTo("back")} />
33 | }
34 | backContent={
35 | rotateRef.current?.flipTo("front")} />
36 | }
37 | {...rotate3dProps}
38 | />
39 | {children}
40 |
41 | );
42 | }
43 |
44 | const ItemWrapper = ({
45 | children,
46 | style,
47 | }: {
48 | children: React.ReactNode;
49 | style?: any;
50 | }) => {
51 | const text = useThemeColor("text");
52 | const itemStyle = { borderColor: text + "24" };
53 | return {children};
54 | };
55 |
56 | const FrontContent = ({ goForward }: { goForward: () => void }) => (
57 |
58 |
59 |
60 | Confirm Delivery
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 |
72 | const BackContent = ({ goBack }: { goBack: () => void }) => {
73 | const padRef = useRef(null);
74 | const pathLength = useSharedValue(0);
75 | const text = useThemeColor("slackText");
76 |
77 | const clearPad = () => {
78 | padRef.current?.erase();
79 | };
80 |
81 | return (
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | Sign
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | );
102 | };
103 |
104 | const InputField = ({
105 | label,
106 | ...props
107 | }: {
108 | label?: string;
109 | } & React.ComponentProps) => {
110 | const text = useThemeColor("text");
111 |
112 | return (
113 |
114 | {label && {label}}
115 |
116 |
120 |
121 |
122 | );
123 | };
124 |
125 | const SubmitButton = ({
126 | onPress,
127 | title = "Next",
128 | hideIcon = false,
129 | }: {
130 | onPress?: () => void;
131 | title?: string;
132 | hideIcon?: boolean;
133 | }) => {
134 | const text = useThemeColor("text");
135 | return (
136 |
137 |
138 |
139 | {title}
140 |
141 | {!hideIcon && (
142 |
143 |
144 |
145 | )}
146 |
147 |
148 | );
149 | };
150 |
151 | const styles = StyleSheet.create({
152 | container: {
153 | flexGrow: 1,
154 | alignItems: "center",
155 | justifyContent: "center",
156 | },
157 | rotateCard: {
158 | width: 320,
159 | height: 364,
160 | margin: 40,
161 | },
162 | itemStyle: {
163 | flex: 1,
164 | borderRadius: 30,
165 | borderCurve: "continuous",
166 | borderWidth: 1.8,
167 | boxShadow: "0 0px 8px rgba(0,0,0,0.05)",
168 | padding: 18,
169 | overflow: "hidden",
170 | },
171 | backCard: {
172 | borderStyle: "dashed",
173 | boxShadow: "none",
174 | },
175 | frontCard: {},
176 | header: {
177 | flexDirection: "row",
178 | alignItems: "center",
179 | justifyContent: "space-between",
180 | marginBottom: 16,
181 | },
182 | title: {
183 | fontSize: 22,
184 | },
185 | backTitle: {
186 | position: "absolute",
187 | left: 0,
188 | right: 0,
189 | textAlign: "center",
190 | pointerEvents: "none",
191 | },
192 | body: {
193 | flex: 1,
194 | gap: 12,
195 | },
196 | inputField: {
197 | gap: 6,
198 | },
199 | label: {
200 | opacity: 0.6,
201 | fontSize: 14,
202 | },
203 | input: {
204 | fontSize: 16,
205 | padding: 12,
206 | borderRadius: 12,
207 | borderCurve: "continuous",
208 | },
209 | button: {
210 | padding: 12,
211 | borderRadius: 12,
212 | borderCurve: "continuous",
213 | alignItems: "center",
214 | justifyContent: "center",
215 | flexDirection: "row",
216 | gap: 6,
217 | marginTop: 2,
218 | // alignSelf: "flex-end",
219 | // width: 150,
220 | },
221 | });
222 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@react-navigation/native";
2 | import { useFonts } from "expo-font";
3 | import { StatusBar } from "expo-status-bar";
4 | import "react-native-reanimated";
5 | import { useColorScheme } from "@/hooks/useColorScheme";
6 | import { SafeAreaProvider } from "react-native-safe-area-context";
7 | import { Drawer } from "expo-router/drawer";
8 | import { useWindowDimensions } from "react-native";
9 | import { GestureHandlerRootView } from "react-native-gesture-handler";
10 | import { DarkTheme, LightTheme } from "@/constants/Theme";
11 | import DrawerContent from "@/components/DrawerContent";
12 | import { useThemeColor } from "@/hooks/useThemeColor";
13 | import { KeyboardProvider } from "react-native-keyboard-controller";
14 | import { NotifyProvider } from "@/components/notify";
15 | import HeadComponent from "@/components/HeadComponent";
16 |
17 | const edgeSwipe = {
18 | swipeEdgeWidth: 20,
19 | };
20 |
21 | export default function RootLayout() {
22 | const colorScheme = useColorScheme();
23 |
24 | const [loaded] = useFonts({
25 | InterRegular: require("../assets/fonts/Inter-Regular.ttf"),
26 | InterMedium: require("../assets/fonts/Inter-Medium.ttf"),
27 | InterSemiBold: require("../assets/fonts/Inter-SemiBold.ttf"),
28 | InterBold: require("../assets/fonts/Inter-Bold.ttf"),
29 | });
30 |
31 | if (!loaded) {
32 | return null;
33 | }
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export function NavigationDrawer() {
55 | const { width } = useWindowDimensions();
56 | const text = useThemeColor("text");
57 | const theme = useColorScheme();
58 | const isLight = theme === "light";
59 |
60 | return (
61 | }
84 | >
85 |
92 |
99 |
105 |
111 |
118 |
124 |
130 |
137 |
143 |
150 |
156 |
164 |
170 |
176 |
190 |
196 |
202 |
208 |
214 |
222 |
223 | );
224 | }
225 |
--------------------------------------------------------------------------------
/components/re-captcha/tray.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet } from "react-native";
2 | import React, { useRef } from "react";
3 | import { ThemedView, ThemedViewWrapper } from "../ThemedView";
4 | import Animated, {
5 | useAnimatedStyle,
6 | useSharedValue,
7 | withTiming,
8 | } from "react-native-reanimated";
9 | import { ReCaptchaProps } from "./config";
10 | import { ThemedText, ThemedTextWrapper } from "../ThemedText";
11 | import Button from "../ui/Button";
12 | import { Ionicons } from "@expo/vector-icons";
13 | import { Headphones, Info, RotateCw } from "lucide-react-native";
14 | import PressableBounce from "../PresableBounce";
15 | import DrawPad, { DrawPadHandle } from "expo-drawpad";
16 | import { Feedback } from "@/functions";
17 | import Dots from "../ui/dots";
18 | import UnderlayText from "./underlay-text";
19 | import { useThemeColor } from "@/hooks/useThemeColor";
20 | import Shimmer from "./shimmer";
21 |
22 | const AnimatedThemedView = Animated.createAnimatedComponent(ThemedView);
23 | const AnimatedThemedText = Animated.createAnimatedComponent(ThemedText);
24 | const AnimatedThemedViewWrapper =
25 | Animated.createAnimatedComponent(ThemedViewWrapper);
26 |
27 | const TRAY_HEIGHT = 320;
28 |
29 | export default function Tray({ shrinkProgress, isVerified }: ReCaptchaProps) {
30 | const padRef = useRef(null);
31 | const animatedStyle = useAnimatedStyle(() => {
32 | return {
33 | opacity: shrinkProgress.value,
34 | marginTop: (shrinkProgress.value - 1) * TRAY_HEIGHT,
35 | transform: [
36 | {
37 | translateY: (1 - shrinkProgress.value) * TRAY_HEIGHT * 1.2,
38 | },
39 | ],
40 | };
41 | });
42 | const text = useThemeColor("text");
43 | const isVerifying = useSharedValue(false);
44 |
45 | const iconProps = {
46 | size: 16,
47 | strokeWidth: 2.7,
48 | };
49 |
50 | const handleResetPad = () => {
51 | padRef.current?.erase();
52 | Feedback.medium();
53 | };
54 |
55 | const dotAnimatedStyle = useAnimatedStyle(() => {
56 | return {
57 | zIndex: isVerifying.value ? 10 : 0,
58 | };
59 | });
60 |
61 | const shimmerAnimatedStyle = useAnimatedStyle(() => {
62 | return {
63 | zIndex: isVerifying.value ? 0 : -1,
64 | };
65 | });
66 |
67 | const btnAnimatedStyle = useAnimatedStyle(() => {
68 | return {
69 | opacity: isVerifying.value ? 0.5 : 1,
70 | };
71 | });
72 |
73 | function getBtnTextAnimatedStyle(show: boolean) {
74 | return useAnimatedStyle(() => ({
75 | opacity: withTiming(isVerifying.value === show ? 1 : 0, {
76 | duration: 100,
77 | }),
78 | }));
79 | }
80 | const btnText1AnimatedStyle = getBtnTextAnimatedStyle(false);
81 | const btnText2AnimatedStyle = getBtnTextAnimatedStyle(true);
82 |
83 | const handleVerify = () => {
84 | Feedback.medium();
85 | isVerifying.value = true;
86 | setTimeout(() => {
87 | isVerifying.value = false;
88 | isVerified.value = true;
89 | Feedback.success();
90 | }, 1500);
91 | };
92 |
93 | return (
94 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | Connect the Numbers in Order
106 |
107 |
108 | (1 to 8)
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
142 |
143 |
150 | Verify
151 |
152 |
159 | Verifying
160 |
161 |
162 |
163 |
164 |
165 | );
166 | }
167 |
168 | type IconButtonProps = {
169 | children: React.ReactElement;
170 | onPress?: () => void;
171 | };
172 |
173 | const IconButton: React.FC = ({ children, onPress }) => (
174 |
178 |
179 | {children}
180 |
181 |
182 | );
183 |
184 | const styles = StyleSheet.create({
185 | container: {
186 | width: 310,
187 | height: TRAY_HEIGHT,
188 | borderRadius: 20,
189 | borderCurve: "continuous",
190 | padding: 4,
191 | paddingBottom: 0,
192 | boxShadow: "0 15px 20px -5px rgba(0,0,0,0.12)",
193 | },
194 | roundText: {
195 | fontFamily: "ui-rounded",
196 | },
197 | content: {
198 | flex: 1,
199 | borderRadius: 16,
200 | borderCurve: "continuous",
201 | padding: 12,
202 | },
203 | footer: {
204 | padding: 12,
205 | flexDirection: "row",
206 | },
207 | title: {
208 | fontWeight: "600",
209 | fontSize: 17,
210 | },
211 | fadeText: {
212 | opacity: 0.4,
213 | fontSize: 12,
214 | letterSpacing: -0.4,
215 | fontWeight: "500",
216 | marginTop: 2,
217 | },
218 | shadow: {
219 | boxShadow: "1px 1px 1px rgba(0,0,0,0.05)",
220 | },
221 | toolBar: {
222 | flexDirection: "row",
223 | alignItems: "center",
224 | flex: 1,
225 | gap: 6,
226 | },
227 | iconButton: {
228 | width: 38,
229 | borderRadius: "50%",
230 | alignItems: "center",
231 | justifyContent: "center",
232 | aspectRatio: 1,
233 | },
234 | verifyButton: {
235 | width: 100,
236 | paddingVertical: 8,
237 | borderRadius: 30,
238 | borderCurve: "continuous",
239 | justifyContent: "center",
240 | alignItems: "center",
241 | },
242 | verifyText: {
243 | color: "#fff",
244 | fontWeight: "600",
245 | fontSize: 16,
246 | position: "absolute",
247 | },
248 | pad: {
249 | flex: 1,
250 | },
251 | underLay: {
252 | ...StyleSheet.absoluteFillObject,
253 | opacity: 0.2,
254 | },
255 | });
256 |
--------------------------------------------------------------------------------