1import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect";
2import type { ReactNode, Ref } from "react";
3import {
4 Platform,
5 View,
6 type ColorValue,
7 type ViewInstance,
8 type ViewProps,
9 type ViewStyle,
10} from "react-native";
11import { withUniwind } from "uniwind";
12
13import { cn } from "../lib/cn";
14import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
15import { GlassBackdrop } from "./GlassBackdrop";
16
17// Explicit mappings keep the native glassEffectStyle enum out of style-array conversion.
18const ThemedGlassView = withUniwind(GlassView, {
19 style: { fromClassName: "className" },
20 tintColor: { fromClassName: "tintColorClassName", styleProperty: "accentColor" },
21});
22
23interface GlassSurfaceProps extends ViewProps {
24 readonly ref?: Ref<ViewInstance>;
25 readonly children: ReactNode;
26 readonly glassEffectStyle?: "clear" | "regular" | "none";
27 readonly tintColor?: ColorValue;
28 readonly tintColorClassName?: string;
29 readonly chrome?: "default" | "none";
30 /** Base color for the frosted tint, or solid fill when blur is unavailable. */
31 readonly fallbackColor?: ColorValue;
32 /** Uniwind styling used only when native Liquid Glass is unavailable. */
33 readonly fallbackClassName?: string;
34}
35
36export function GlassSurface({
37 ref,
38 children,
39 glassEffectStyle = "regular",
40 chrome = "default",
41 tintColor,
42 tintColorClassName,
43 fallbackColor,
44 fallbackClassName,
45 className,
46 style,
47 ...props
48}: GlassSurfaceProps) {
49 const { themeAppearance } = useAppearancePreferences();
50 const isDarkMode = themeAppearance === "dark";
51 const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable();
52 const hasShadow = chrome !== "none" && Platform.OS !== "android";
53 const surfaceStyle: ViewStyle = {
54 borderRadius: 32,
55 overflow: "hidden",
56 shadowColor: hasShadow ? "#000000" : "transparent",
57 shadowOpacity: hasShadow ? (isDarkMode ? 0.22 : 0.08) : 0,
58 shadowRadius: hasShadow ? 28 : 0,
59 shadowOffset: { width: 0, height: hasShadow ? 14 : 0 },
60 elevation: hasShadow ? 12 : 0,
61 };
62
63 if (supportsGlass) {
64 return (
65 <ThemedGlassView
66 {...props}
67 ref={ref}
68 className={cn(
69 chrome === "none"
70 ? "border-0 border-transparent bg-transparent"
71 : "border border-border bg-glass-surface",
72 className,
73 )}
74 glassEffectStyle={glassEffectStyle}
75 tintColor={tintColor === undefined ? undefined : String(tintColor)}
76 tintColorClassName={
77 tintColorClassName ?? (tintColor === undefined ? "accent-glass-tint" : undefined)
78 }
79 colorScheme={isDarkMode ? "dark" : "light"}
80 style={[surfaceStyle, style]}
81 >
82 {children}
83 </ThemedGlassView>
84 );
85 }
86
87 return (
88 <View
89 {...props}
90 ref={ref}
91 className={cn(
92 chrome === "none" ? "border-0 border-transparent" : "border border-border",
93 fallbackClassName,
94 className,
95 )}
96 style={[surfaceStyle, style]}
97 >
98 <GlassBackdrop fallbackColor={fallbackColor} />
99 {children}
100 </View>
101 );
102}