Edge-lit liquid-glass card with a rubber-band drag handle — tracks the finger, resists at 20%, pours home
components/ui/glass-sheet-edge/glass-sheet-edge.tsx@lumioui/core@latestreact-native-reanimated@*| travel? | number | Max downward travel in pt before the rubber-band dominates. Default 140. |
| onRelease? | (dragged: number) => void | |
| className? | string | |
| children? | React.ReactNode |
Edge-lit liquid-glass card with a rubber-band drag handle — tracks the finger, resists at 20%, pours home
import { GlassSheetEdge } from "@/components/ui/glass-sheet-edge/glass-sheet-edge";
export function Example() {
return (
<GlassSheetEdge />
);
} import * as React from "react";
import { forwardRef } from "react";
import { PanResponder, View, type ViewProps } from "react-native";
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
} from "react-native-reanimated";
import { cn, spring, useHaptic, useReducedMotion } from "@lumioui/core";
import { Glass } from "../glass";
/**
* GlassSheetEdge — an edge-lit liquid-glass card that can be grabbed by
* its handle and rubber-banded: it tracks the finger, resists past its
* travel limit at 20%, then springs home. Demonstrates the material's
* "pullable" edge on overlays.
*
* <GlassSheetEdge>…content…</GlassSheetEdge>
* Expo Go compatible.
*/
export interface GlassSheetEdgeProps extends Omit<ViewProps, "className"> {
/** Max downward travel in pt before the rubber-band dominates. Default 140. */
travel?: number;
onRelease?: (dragged: number) => void;
className?: string;
children?: React.ReactNode;
}
export const GlassSheetEdge = forwardRef<
React.ElementRef<typeof View>,
GlassSheetEdgeProps
>(function GlassSheetEdge(
{ travel = 140, onRelease, className, children, ...props },
ref
) {
const haptics = useHaptic();
const reduce = useReducedMotion();
const drag = useSharedValue(0);
const pan = React.useMemo(
() =>
PanResponder.create({
onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dy) > 8,
onPanResponderMove: (_, g) => {
if (reduce) return;
// 1:1 inside travel, 20% resistance beyond — the liquid "pull"
const d = g.dy;
drag.value = Math.abs(d) <= travel ? d : Math.sign(d) * (travel + (Math.abs(d) - travel) * 0.2);
},
onPanResponderRelease: (_, g) => {
onRelease?.(g.dy);
haptics.light();
drag.value = reduce ? 0 : withSpring(0, spring.bouncy);
},
onPanResponderTerminate: () => {
drag.value = reduce ? 0 : withSpring(0, spring.bouncy);
},
}),
[drag, travel, reduce, haptics, onRelease]
);
const style = useAnimatedStyle(() => ({
transform: [{ translateY: drag.value }],
}));
// the sheet "bends": mild squash at the drag extremes
const bendStyle = useAnimatedStyle(() => ({
transform: [{ scaleY: 1 - Math.min(Math.abs(drag.value) / (travel * 4), 0.06) }],
}));
return (
<Animated.View
ref={ref}
style={style}
className={cn("overflow-hidden rounded-3xl", className)}
{...props}
>
<Glass effect="regular" sheen={0.7} className="absolute inset-0 rounded-3xl" />
<Animated.View style={bendStyle}>
<View
{...pan.panHandlers}
accessibilityRole="adjustable"
accessibilityLabel="Drag handle — pull to stretch the sheet"
className="items-center pb-1 pt-3"
>
<View className="h-1.5 w-10 rounded-full bg-foreground/25" />
</View>
{children}
</Animated.View>
</Animated.View>
);
});
GlassSheetEdge.displayName = "GlassSheetEdge";