{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-toast",
  "title": "Glass Toast",
  "description": "A glass notification for brief updates, announced to screen readers.",
  "dependencies": [
    "react-native-safe-area-context",
    "react-native-svg"
  ],
  "registryDependencies": [
    "@glassyui-native/glass-panel",
    "@glassyui-native/glass-provider"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-toast.tsx",
      "content": "import {\n  Children,\n  createContext,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useEffectEvent,\n  useId,\n  useMemo,\n  useState,\n  type ForwardedRef,\n  type ReactElement,\n  type ReactNode,\n} from 'react';\nimport {\n  AccessibilityInfo,\n  Animated,\n  PanResponder,\n  Platform,\n  Pressable,\n  StyleSheet,\n  Text,\n  useWindowDimensions,\n  View,\n  type GestureResponderEvent,\n  type LayoutChangeEvent,\n  type PanResponderGestureState,\n  type PressableProps,\n  type StyleProp,\n  type TextProps,\n  type TextStyle,\n  type ViewProps,\n  type ViewStyle,\n} from 'react-native';\nimport { GlassPanel } from './glass-panel';\nimport { useGlassTheme } from './glass-provider';\nimport { GlassIcon } from './lib/icons';\nimport {\n  CONTROL_SURFACE_PRESSED,\n  LAYER_EASING,\n  useLayerPresence,\n  useSafeInsets,\n} from './lib/modal-layer';\nimport { GLASS_MOTION } from './lib/tokens';\nimport { useControllableState } from './lib/use-controllable-state';\n\ntype ToastEntry = { element: ReactElement; id: string };\n\ntype ToastRegistry = {\n  duration: number;\n  remove: (id: string) => void;\n  swipeThreshold: number;\n  upsert: (id: string, element: ReactElement) => void;\n};\n\nconst ToastRegistryContext = createContext<ToastRegistry | null>(null);\n\nconst ToastEntriesContext = createContext<readonly ToastEntry[] | null>(null);\n\n// Dismisses the toast that a close or action control sits in.\nconst ToastDismissContext = createContext<(() => void) | null>(null);\n\n// A flick up faster than this, in points per millisecond, dismisses a toast however short.\nconst FLING_VELOCITY = 0.5;\n\n// Where a swipe ended: its distance and its speed, in points per millisecond.\ntype GestureRelease = { dy: number; vy: number };\n\n// The dismiss button's fill under a finger, from liquid.css.\nconst CLOSE_PRESSED = { dark: 'rgba(255, 255, 255, 0.14)', light: 'rgba(0, 0, 0, 0.07)' } as const;\n\nexport type GlassToastProviderProps = {\n  children?: ReactNode;\n  /** How long a toast stays, in milliseconds. Infinity keeps it until dismissed. Defaults to 5000. */\n  duration?: number;\n  /** How far a swipe up must travel to dismiss a toast, in points. Defaults to 50. */\n  swipeThreshold?: number;\n};\n\n/** Collects the toasts declared anywhere beneath it for GlassToastViewport to show. */\nexport function GlassToastProvider({\n  children,\n  duration = 5000,\n  swipeThreshold = 50,\n}: GlassToastProviderProps) {\n  const [entries, setEntries] = useState<readonly ToastEntry[]>([]);\n\n  const upsert = useCallback((id: string, element: ReactElement) => {\n    setEntries((current) =>\n      current.some((entry) => entry.id === id)\n        ? current.map((entry) => (entry.id === id ? { element, id } : entry))\n        : [...current, { element, id }],\n    );\n  }, []);\n\n  const remove = useCallback((id: string) => {\n    setEntries((current) => current.filter((entry) => entry.id !== id));\n  }, []);\n\n  const registry = useMemo(\n    () => ({ duration, remove, swipeThreshold, upsert }),\n    [duration, remove, swipeThreshold, upsert],\n  );\n\n  return (\n    <ToastRegistryContext.Provider value={registry}>\n      <ToastEntriesContext.Provider value={entries}>{children}</ToastEntriesContext.Provider>\n    </ToastRegistryContext.Provider>\n  );\n}\n\nfunction useToastRegistry() {\n  const registry = useContext(ToastRegistryContext);\n\n  if (!registry) throw new Error('GlassToast must be used within GlassToastProvider');\n\n  return registry;\n}\n\nfunction useToastEntries() {\n  const entries = useContext(ToastEntriesContext);\n\n  if (!entries) throw new Error('GlassToastViewport must be used within GlassToastProvider');\n\n  return entries;\n}\n\nfunction useToastDismiss(component: string) {\n  const dismiss = useContext(ToastDismissContext);\n\n  if (!dismiss) throw new Error(`${component} must be used within GlassToast`);\n\n  return dismiss;\n}\n\n/** Shows the provider's toasts at the top of the screen. Place it once, near the app root. */\nexport const GlassToastViewport = forwardRef<View, ViewProps>(({ style, ...rest }, ref) => {\n  const entries = useToastEntries();\n  const insets = useSafeInsets();\n\n  return (\n    <View\n      ref={ref}\n      style={[\n        { pointerEvents: 'box-none' },\n        styles.viewport,\n        {\n          paddingEnd: insets.end + 16,\n          paddingStart: insets.start + 16,\n          paddingTop: insets.top + 8,\n        },\n        style,\n      ]}\n      {...rest}\n    >\n      {entries.map((entry) => entry.element)}\n    </View>\n  );\n});\n\nGlassToastViewport.displayName = 'GlassToastViewport';\n\nexport type GlassToastProps = ViewProps & {\n  children?: ReactNode;\n  /** Whether it shows when it mounts, unless `open` is set. Defaults to true, as on the web. */\n  defaultOpen?: boolean;\n  /** Overrides the provider's duration for this toast. */\n  duration?: number;\n  onOpenChange?: (open: boolean) => void;\n  open?: boolean;\n};\n\ntype ToastTextSource = { altText?: string; children?: ReactNode };\n\n// What a screen reader hears when a toast appears: its words, with an action's altText in place\n// of the button, as on the web.\nfunction toastText(node: ReactNode): string {\n  const words: string[] = [];\n\n  for (const child of Children.toArray(node)) {\n    if (!isValidElement<ToastTextSource>(child)) {\n      words.push(String(child));\n    } else if (child.type === GlassToastAction) {\n      words.push(child.props.altText ?? '');\n    } else if (child.type !== GlassToastClose) {\n      words.push(toastText(child.props.children));\n    }\n  }\n\n  return words.filter(Boolean).join(' ');\n}\n\n/** A glass notification, shown in the viewport, that leaves after its duration or a swipe up. */\nexport const GlassToast = forwardRef<View, GlassToastProps>(\n  ({ children, defaultOpen = true, duration, onOpenChange, open, ...rest }, ref) => {\n    const registry = useToastRegistry();\n    const id = useId();\n    const [isOpen, setOpen] = useControllableState(open, defaultOpen, onOpenChange);\n    const { remove, upsert } = registry;\n\n    const element = (\n      <ToastCard\n        key={id}\n        announcement={toastText(children)}\n        duration={duration ?? registry.duration}\n        onOpenChange={setOpen}\n        open={isOpen}\n        panelRef={ref}\n        swipeThreshold={registry.swipeThreshold}\n        {...rest}\n      >\n        {children}\n      </ToastCard>\n    );\n\n    // React Native has no portals: every render hands the viewport this toast's latest props.\n    useEffect(() => {\n      upsert(id, element);\n    });\n\n    useEffect(() => () => remove(id), [id, remove]);\n\n    return null;\n  },\n);\n\nGlassToast.displayName = 'GlassToast';\n\ntype ToastCardProps = ViewProps & {\n  announcement: string;\n  children?: ReactNode;\n  duration: number;\n  onOpenChange: (open: boolean) => void;\n  open: boolean;\n  panelRef: ForwardedRef<View>;\n  swipeThreshold: number;\n};\n\n// Keeps a closing toast in the viewport until it has slid away.\nfunction ToastCard({ open, ...props }: ToastCardProps) {\n  const { preferences } = useGlassTheme();\n  const [present, hide] = useLayerPresence(open, preferences.reduceMotion);\n\n  return present ? <ToastLayer {...props} onHidden={hide} open={open} /> : null;\n}\n\ntype ToastLayerProps = ToastCardProps & { onHidden: () => void };\n\n// Mounted while the toast is on screen, so each showing measures the toast afresh.\nfunction ToastLayer({\n  announcement,\n  children,\n  duration,\n  onHidden,\n  onOpenChange,\n  open,\n  panelRef,\n  style,\n  swipeThreshold,\n  ...rest\n}: ToastLayerProps) {\n  const { colors, preferences } = useGlassTheme();\n  const { height } = useWindowDimensions();\n  const [progress] = useState(() => new Animated.Value(0));\n  const [swipe] = useState(() => new Animated.Value(0));\n  // How far the toast's lower edge sits below the viewport's top, once it has been laid out.\n  const [reach, setReach] = useState<number | null>(null);\n  // A finger is on the toast, so its timer waits.\n  const [held, setHeld] = useState(false);\n  const [released, setReleased] = useState<GestureRelease | null>(null);\n  const reduceMotion = preferences.reduceMotion;\n  const measured = reach !== null;\n  // Until the toast has measured itself, a whole screen up keeps it out of sight.\n  const travel = (reach ?? height) + 16;\n  const dismiss = useCallback(() => onOpenChange(false), [onOpenChange]);\n  const expire = useEffectEvent(() => onOpenChange(false));\n\n  useEffect(() => {\n    if (reduceMotion) {\n      progress.setValue(1);\n      swipe.setValue(0);\n\n      return;\n    }\n\n    if (!measured) {\n      // Closed before it was laid out, so there is nothing on screen to slide away.\n      if (!open) onHidden();\n\n      return;\n    }\n\n    const slide = Animated.timing(progress, {\n      duration: open ? 420 : 220,\n      easing: open ? LAYER_EASING.out : LAYER_EASING.in,\n      toValue: open ? 1 : 0,\n      useNativeDriver: GLASS_MOTION.nativeDriver,\n    });\n\n    // Shown again after a fling, the toast comes back from where it flew. A new swipe stops only\n    // that return, not the slide.\n    const animation = open\n      ? Animated.parallel(\n          [\n            slide,\n            Animated.timing(swipe, {\n              duration: 420,\n              easing: LAYER_EASING.out,\n              toValue: 0,\n              useNativeDriver: GLASS_MOTION.nativeDriver,\n            }),\n          ],\n          { stopTogether: false },\n        )\n      : slide;\n\n    animation.start(({ finished }) => {\n      if (finished && !open) onHidden();\n    });\n\n    return () => animation.stop();\n  }, [measured, onHidden, open, progress, reduceMotion, swipe]);\n\n  useEffect(() => {\n    if (!open || held || !Number.isFinite(duration) || duration <= 0) return;\n\n    const timer = setTimeout(() => expire(), duration);\n\n    return () => clearTimeout(timer);\n  }, [duration, held, open]);\n\n  // Android reads the toast through its polite live region; iOS has no live regions, so the\n  // toast announces itself there. Doing both would read it twice on Android.\n  useEffect(() => {\n    if (Platform.OS !== 'android' && open && announcement) {\n      AccessibilityInfo.announceForAccessibility(announcement);\n    }\n  }, [announcement, open]);\n\n  // The responder is built once, so a re-render mid-swipe cannot reset the gesture. It hands each\n  // release to an effect, which reads the latest props.\n  const [swiper] = useState(() => {\n    const release = (_event: GestureResponderEvent, gesture: PanResponderGestureState) => {\n      setHeld(false);\n      setReleased({ dy: gesture.dy, vy: gesture.vy });\n    };\n\n    return PanResponder.create({\n      // Only a swipe up moves the toast, so taps reach its buttons.\n      onMoveShouldSetPanResponder: (_event, gesture) =>\n        gesture.dy < -8 && Math.abs(gesture.dy) > Math.abs(gesture.dx),\n      onPanResponderGrant: () => {\n        swipe.stopAnimation();\n        setHeld(true);\n      },\n      onPanResponderMove: (_event, gesture) => swipe.setValue(Math.min(0, gesture.dy)),\n      onPanResponderRelease: release,\n      onPanResponderTerminate: release,\n      onPanResponderTerminationRequest: () => false,\n    });\n  });\n\n  const settle = useEffectEvent(({ dy, vy }: GestureRelease) => {\n    if (-dy > swipeThreshold || vy < -FLING_VELOCITY) {\n      if (reduceMotion) {\n        dismiss();\n\n        return;\n      }\n\n      Animated.timing(swipe, {\n        duration: 160,\n        easing: LAYER_EASING.out,\n        toValue: -travel,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }).start(({ finished }) => {\n        if (finished) dismiss();\n      });\n\n      return;\n    }\n\n    if (reduceMotion) {\n      swipe.setValue(0);\n\n      return;\n    }\n\n    Animated.spring(swipe, {\n      ...GLASS_MOTION.spring,\n      toValue: 0,\n      useNativeDriver: GLASS_MOTION.nativeDriver,\n    }).start();\n  });\n\n  useEffect(() => {\n    if (released) settle(released);\n  }, [released]);\n\n  const handleLayout = (event: LayoutChangeEvent) => {\n    const { height: toastHeight, y } = event.nativeEvent.layout;\n\n    setReach(y + toastHeight);\n  };\n\n  // The dismiss button stands beside the words, not under them.\n  const message: ReactNode[] = [];\n  const closers: ReactNode[] = [];\n\n  for (const child of Children.toArray(children)) {\n    if (isValidElement(child) && child.type === GlassToastClose) {\n      closers.push(child);\n    } else {\n      message.push(child);\n    }\n  }\n\n  return (\n    <ToastDismissContext.Provider value={dismiss}>\n      <Animated.View\n        {...swiper.panHandlers}\n        onLayout={handleLayout}\n        style={[\n          styles.slot,\n          {\n            transform: [\n              {\n                translateY: Animated.add(\n                  progress.interpolate({ inputRange: [0, 1], outputRange: [-travel, 0] }),\n                  swipe,\n                ),\n              },\n            ],\n          },\n        ]}\n      >\n        <GlassPanel\n          ref={panelRef}\n          accessibilityLiveRegion=\"polite\"\n          elevation=\"raised\"\n          preset=\"overlay\"\n          variant=\"card\"\n          style={[styles.toast, style]}\n          {...rest}\n        >\n          <View style={styles.message}>\n            {setInText(message, [styles.messageText, { color: colors.foreground }])}\n          </View>\n          {closers}\n        </GlassPanel>\n      </Animated.View>\n    </ToastDismissContext.Provider>\n  );\n}\n\n// React Native renders a bare string only inside Text.\nfunction setInText(children: ReactNode, style: StyleProp<TextStyle>) {\n  return Children.map(children, (child) =>\n    child === null || child instanceof Object ? child : <Text style={style}>{child}</Text>,\n  );\n}\n\n/** The toast's headline. */\nexport const GlassToastTitle = forwardRef<Text, TextProps>(({ style, ...rest }, ref) => {\n  const { colors } = useGlassTheme();\n\n  return <Text ref={ref} style={[styles.title, { color: colors.foreground }, style]} {...rest} />;\n});\n\nGlassToastTitle.displayName = 'GlassToastTitle';\n\n/** The toast's supporting text. */\nexport const GlassToastDescription = forwardRef<Text, TextProps>(({ style, ...rest }, ref) => {\n  const { colors } = useGlassTheme();\n\n  return (\n    <Text\n      ref={ref}\n      style={[styles.description, { color: colors.mutedForeground }, style]}\n      {...rest}\n    />\n  );\n});\n\nGlassToastDescription.displayName = 'GlassToastDescription';\n\nexport type GlassToastActionProps = Omit<PressableProps, 'children' | 'style'> & {\n  /**\n   * How to do the same without the button, for screen-reader users who may not reach it before\n   * the toast leaves. It is announced with the toast and read as the button's hint.\n   */\n  altText: string;\n  children?: ReactNode;\n  style?: StyleProp<ViewStyle>;\n};\n\n/** A pill under the message, such as Undo. Pressing it runs `onPress` and dismisses the toast. */\nexport const GlassToastAction = forwardRef<View, GlassToastActionProps>(\n  ({ altText, children, onPress, onPressIn, onPressOut, style, ...rest }, ref) => {\n    const dismiss = useToastDismiss('GlassToastAction');\n    const { colors, preferences } = useGlassTheme();\n    const [scale] = useState(() => new Animated.Value(1));\n\n    const settle = (toValue: number) => {\n      if (preferences.reduceMotion) return;\n\n      Animated.spring(scale, {\n        ...GLASS_MOTION.spring,\n        toValue,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }).start();\n    };\n\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityHint={altText}\n        accessibilityRole=\"button\"\n        hitSlop={7}\n        onPress={(event) => {\n          onPress?.(event);\n          dismiss();\n        }}\n        onPressIn={(event) => {\n          onPressIn?.(event);\n          settle(0.94);\n        }}\n        onPressOut={(event) => {\n          onPressOut?.(event);\n          settle(1);\n        }}\n        style={[styles.actionRoot, style]}\n        {...rest}\n      >\n        <Animated.View\n          style={[\n            styles.action,\n            {\n              backgroundColor: CONTROL_SURFACE_PRESSED[preferences.theme],\n              boxShadow: `inset 0px 0px 0px 1px ${colors.border}`,\n              transform: [{ scale }],\n            },\n          ]}\n        >\n          <Text numberOfLines={1} style={[styles.actionLabel, { color: colors.foreground }]}>\n            {children}\n          </Text>\n        </Animated.View>\n      </Pressable>\n    );\n  },\n);\n\nGlassToastAction.displayName = 'GlassToastAction';\n\nexport type GlassToastCloseProps = Omit<PressableProps, 'children' | 'style'> & {\n  /** Replaces the drawn ×; its text then names the button. */\n  children?: ReactNode;\n  style?: StyleProp<ViewStyle>;\n};\n\n/** A round button that dismisses the toast. It draws its own × and is named \"Dismiss\". */\nexport const GlassToastClose = forwardRef<View, GlassToastCloseProps>(\n  ({ accessibilityLabel, children, onPress, style, ...rest }, ref) => {\n    const dismiss = useToastDismiss('GlassToastClose');\n    const { colors, preferences } = useGlassTheme();\n    const drawn = children === undefined;\n\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityLabel={accessibilityLabel ?? (drawn ? 'Dismiss' : undefined)}\n        accessibilityRole=\"button\"\n        hitSlop={8}\n        onPress={(event) => {\n          onPress?.(event);\n          dismiss();\n        }}\n        style={({ pressed }) => [\n          styles.close,\n          !drawn && styles.closeWithText,\n          pressed && { backgroundColor: CLOSE_PRESSED[preferences.theme] },\n          style,\n        ]}\n        {...rest}\n      >\n        {({ pressed }) =>\n          drawn ? (\n            <GlassIcon\n              color={pressed ? colors.foreground : colors.mutedForeground}\n              name=\"close\"\n              size={16}\n              strokeWidth={1.6}\n            />\n          ) : (\n            setInText(children, [\n              styles.closeLabel,\n              { color: pressed ? colors.foreground : colors.mutedForeground },\n            ])\n          )\n        }\n      </Pressable>\n    );\n  },\n);\n\nGlassToastClose.displayName = 'GlassToastClose';\n\nconst styles = StyleSheet.create({\n  action: {\n    alignItems: 'center',\n    borderCurve: 'continuous',\n    borderRadius: 999,\n    height: 30,\n    justifyContent: 'center',\n    paddingHorizontal: 14,\n  },\n  actionLabel: { fontSize: 13, fontWeight: '600' },\n  // A 30-point pill answers a 44-point touch.\n  actionRoot: { alignSelf: 'flex-start', marginTop: 10 },\n  close: {\n    alignItems: 'center',\n    borderCurve: 'continuous',\n    borderRadius: 999,\n    height: 28,\n    justifyContent: 'center',\n    marginEnd: -6,\n    marginStart: 12,\n    marginTop: -4,\n    minWidth: 28,\n  },\n  closeLabel: { fontSize: 13, fontWeight: '600' },\n  closeWithText: { paddingHorizontal: 8 },\n  description: { fontSize: 14, lineHeight: 20, marginTop: 4 },\n  message: { flex: 1 },\n  messageText: { fontSize: 14, lineHeight: 20 },\n  slot: { maxWidth: 352, width: '100%' },\n  title: { fontSize: 16, fontWeight: '600', lineHeight: 24 },\n  toast: { alignItems: 'flex-start', flexDirection: 'row' },\n  viewport: {\n    alignItems: 'center',\n    gap: 12,\n    left: 0,\n    position: 'absolute',\n    right: 0,\n    top: 0,\n    zIndex: 100,\n  },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-toast.tsx"
    },
    {
      "path": "registry/glassyui-native/lib/icons.tsx",
      "content": "import { View } from 'react-native';\nimport Svg, { Path } from 'react-native-svg';\n\nexport type GlassIconName =\n  | 'check'\n  | 'chevron-down'\n  | 'chevron-right'\n  | 'close'\n  | 'dash'\n  | 'danger'\n  | 'info'\n  | 'success'\n  | 'warning';\n\n// The web components' own strokes, drawn on a 20-point grid.\nconst PATHS = {\n  check: ['M4.5 10.5 8.25 14.25 15.5 6.25'],\n  'chevron-down': ['M5 7.5l5 5 5-5'],\n  'chevron-right': ['M7.5 4.4 13.1 10l-5.6 5.6'],\n  close: ['M5.5 5.5l9 9m0-9l-9 9'],\n  dash: ['M5 10h10'],\n  danger: ['M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z', 'M7.6 7.6l4.8 4.8', 'M12.4 7.6l-4.8 4.8'],\n  info: ['M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z', 'M10 9.2v4.1', 'M10 6.7h.01'],\n  success: ['M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z', 'M6.9 10.2l2.2 2.2 4-4.4'],\n  warning: ['M10 3.6 17 16H3L10 3.6Z', 'M10 8.4v3.4', 'M10 14h.01'],\n} as const satisfies Record<GlassIconName, readonly string[]>;\n\nexport type GlassIconProps = {\n  color: string;\n  name: GlassIconName;\n  size?: number;\n  strokeWidth?: number;\n};\n\n/** A stroked glyph from the library's small icon set. Decorative; label its control instead. */\nexport function GlassIcon({ color, name, size = 20, strokeWidth = 1.8 }: GlassIconProps) {\n  // The hiding lives on a View: on the web, Svg passes its props to a bare <svg> element.\n  return (\n    <View\n      accessibilityElementsHidden\n      importantForAccessibility=\"no-hide-descendants\"\n      style={{ height: size, width: size }}\n    >\n      <Svg width={size} height={size} viewBox=\"0 0 20 20\" fill=\"none\">\n        {PATHS[name].map((path) => (\n          <Path\n            key={path}\n            d={path}\n            stroke={color}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth={strokeWidth}\n          />\n        ))}\n      </Svg>\n    </View>\n  );\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/icons.tsx"
    },
    {
      "path": "registry/glassyui-native/lib/modal-layer.tsx",
      "content": "import {\n  Children,\n  cloneElement,\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n  type ReactElement,\n  type ReactNode,\n} from 'react';\nimport {\n  Animated,\n  Easing,\n  I18nManager,\n  Modal,\n  Pressable,\n  StyleSheet,\n  Text,\n  type AccessibilityState,\n  type GestureResponderEvent,\n  type ModalProps,\n  type StyleProp,\n  type TextProps,\n  type ViewStyle,\n} from 'react-native';\nimport { initialWindowMetrics, SafeAreaInsetsContext } from 'react-native-safe-area-context';\nimport { useGlassTheme } from '../glass-provider';\nimport { GlassIcon } from './icons';\nimport { useControllableState } from './use-controllable-state';\n\n/** Arrivals ease out on the library's curve (`--lg-ease-out`); exits ease in. */\nexport const LAYER_EASING = {\n  in: Easing.bezier(0.42, 0, 1, 1),\n  out: Easing.bezier(0.32, 0.72, 0, 1),\n};\n\n/** The fill of a control on glass under a finger (`--lg-control-surface-hover`). */\nexport const CONTROL_SURFACE_PRESSED = {\n  dark: 'rgba(245, 245, 247, 0.14)',\n  light: 'rgba(255, 255, 255, 0.95)',\n} as const;\n\n// Without these, iOS turns a landscape screen back to portrait while a layer is open.\nconst ORIENTATIONS = [\n  'portrait',\n  'portrait-upside-down',\n  'landscape',\n  'landscape-left',\n  'landscape-right',\n] satisfies ModalProps['supportedOrientations'];\n\n/**\n * The screen's safe-area insets, with start and end following the layout direction. Reads the\n * nearest SafeAreaProvider, or the window's first measurement when there is none.\n */\nexport function useSafeInsets() {\n  const insets = useContext(SafeAreaInsetsContext) ?? initialWindowMetrics?.insets;\n  const left = insets?.left ?? 0;\n  const right = insets?.right ?? 0;\n\n  return {\n    bottom: insets?.bottom ?? 0,\n    end: I18nManager.isRTL ? left : right,\n    start: I18nManager.isRTL ? right : left,\n    top: insets?.top ?? 0,\n  };\n}\n\n/**\n * Whether a layer is on screen. It turns true with `open` and stays true until the layer calls\n * `hide` at the end of its exit, so the exit can play. Under Reduce Motion it turns false at once.\n */\nexport function useLayerPresence(open: boolean, reduceMotion: boolean) {\n  const [present, setPresent] = useState(open);\n\n  if (open && !present) setPresent(true);\n\n  if (!open && present && reduceMotion) setPresent(false);\n\n  const hide = useCallback(() => setPresent(false), []);\n\n  return [present, hide] as const;\n}\n\ntype ModalLayerState = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n};\n\nconst ModalLayerContext = createContext<ModalLayerState | null>(null);\n\nexport type ModalLayerRootProps = {\n  children?: ReactNode;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  open?: boolean;\n};\n\n/** Holds a modal layer's open state for its trigger, content and close controls. */\nexport function ModalLayerRoot({\n  children,\n  defaultOpen = false,\n  onOpenChange,\n  open,\n}: ModalLayerRootProps) {\n  const [current, setOpen] = useControllableState(open, defaultOpen, onOpenChange);\n  const value = useMemo(() => ({ open: current, setOpen }), [current, setOpen]);\n\n  return <ModalLayerContext.Provider value={value}>{children}</ModalLayerContext.Provider>;\n}\n\n/** The nearest modal layer's state. `component` and `root` name the parts in the error. */\nexport function useModalLayer(component: string, root: string) {\n  const context = useContext(ModalLayerContext);\n\n  if (!context) throw new Error(`${component} must be used within ${root}`);\n\n  return context;\n}\n\ntype PressableChildProps = {\n  accessibilityState?: AccessibilityState;\n  onPress?: ((event: GestureResponderEvent) => void) | null;\n};\n\nexport type ModalLayerSlotProps = {\n  /** One pressable element, such as a GlassButton. It keeps its own onPress. */\n  children: ReactElement<PressableChildProps>;\n};\n\ntype SlotOwnerProps = ModalLayerSlotProps & {\n  component: string;\n  root: string;\n};\n\n/** Gives its child an onPress that toggles the layer, and tells assistive tech whether it is open. */\nexport function ModalLayerTrigger({ children, component, root }: SlotOwnerProps) {\n  const { open, setOpen } = useModalLayer(component, root);\n  const child = Children.only(children);\n\n  return cloneElement(child, {\n    accessibilityState: { ...child.props.accessibilityState, expanded: open },\n    onPress: (event: GestureResponderEvent) => {\n      child.props.onPress?.(event);\n      setOpen(!open);\n    },\n  });\n}\n\n/** Gives its child an onPress that closes the layer. */\nexport function ModalLayerClose({ children, component, root }: SlotOwnerProps) {\n  const { setOpen } = useModalLayer(component, root);\n  const child = Children.only(children);\n\n  return cloneElement(child, {\n    onPress: (event: GestureResponderEvent) => {\n      child.props.onPress?.(event);\n      setOpen(false);\n    },\n  });\n}\n\ntype ModalLayerHostProps = {\n  children: ReactNode;\n  /** Android's back button and the web's Escape key. */\n  onRequestClose: () => void;\n  present: boolean;\n};\n\n/** React Native's Modal, set up for a layer that draws its own scrim and animates itself. */\nexport function ModalLayerHost({ children, onRequestClose, present }: ModalLayerHostProps) {\n  return (\n    <Modal\n      animationType=\"none\"\n      navigationBarTranslucent\n      onRequestClose={onRequestClose}\n      statusBarTranslucent\n      supportedOrientations={ORIENTATIONS}\n      transparent\n      visible={present}\n    >\n      {present ? children : null}\n    </Modal>\n  );\n}\n\ntype ModalLayerScrimProps = {\n  onPress: () => void;\n  /** 0 hidden, 1 shown. */\n  progress: Animated.Value;\n};\n\n/** The dimmed backdrop behind a modal layer. It fades with `progress`; a tap on it closes the layer. */\nexport function ModalLayerScrim({ onPress, progress }: ModalLayerScrimProps) {\n  const { colors } = useGlassTheme();\n\n  return (\n    <>\n      <Animated.View\n        style={[\n          { pointerEvents: 'none' },\n          StyleSheet.absoluteFill,\n          { backgroundColor: colors.scrim, opacity: progress },\n        ]}\n      />\n      <Pressable\n        accessible={false}\n        importantForAccessibility=\"no\"\n        onPress={onPress}\n        style={StyleSheet.absoluteFill}\n      />\n    </>\n  );\n}\n\ntype ModalLayerCloseButtonProps = {\n  label: string;\n  onPress: () => void;\n  /** Places the button; it is absolutely positioned. */\n  style?: StyleProp<ViewStyle>;\n};\n\n/** The round close button in a layer's top corner, with a 44-point target. */\nexport function ModalLayerCloseButton({ label, onPress, style }: ModalLayerCloseButtonProps) {\n  const { colors, preferences } = useGlassTheme();\n\n  return (\n    <Pressable\n      accessibilityLabel={label}\n      accessibilityRole=\"button\"\n      hitSlop={4}\n      onPress={onPress}\n      style={({ pressed }) => [\n        styles.close,\n        {\n          backgroundColor: pressed\n            ? CONTROL_SURFACE_PRESSED[preferences.theme]\n            : colors.controlSurface,\n          borderColor: colors.border,\n        },\n        style,\n      ]}\n    >\n      {({ pressed }) => (\n        <GlassIcon\n          color={pressed ? colors.foreground : colors.mutedForeground}\n          name=\"close\"\n          size={16}\n          strokeWidth={1.6}\n        />\n      )}\n    </Pressable>\n  );\n}\n\n/** A layer's heading, announced as a header and kept clear of the close button. */\nexport const ModalLayerTitle = forwardRef<Text, TextProps>(({ style, ...rest }, ref) => {\n  const { colors } = useGlassTheme();\n\n  return (\n    <Text\n      ref={ref}\n      accessibilityRole=\"header\"\n      style={[styles.title, { color: colors.foreground }, style]}\n      {...rest}\n    />\n  );\n});\n\nModalLayerTitle.displayName = 'ModalLayerTitle';\n\n/** A layer's supporting text, in the muted ink. */\nexport const ModalLayerDescription = forwardRef<Text, TextProps>(({ style, ...rest }, ref) => {\n  const { colors } = useGlassTheme();\n\n  return (\n    <Text\n      ref={ref}\n      style={[styles.description, { color: colors.mutedForeground }, style]}\n      {...rest}\n    />\n  );\n});\n\nModalLayerDescription.displayName = 'ModalLayerDescription';\n\nconst styles = StyleSheet.create({\n  close: {\n    alignItems: 'center',\n    borderCurve: 'continuous',\n    borderRadius: 18,\n    borderWidth: 1,\n    height: 36,\n    justifyContent: 'center',\n    position: 'absolute',\n    width: 36,\n  },\n  description: { fontSize: 15, lineHeight: 21, marginTop: 6 },\n  // The close button sits 16 points in from the corner; the title stops 8 points short of it.\n  title: { fontSize: 20, fontWeight: '600', letterSpacing: -0.3, lineHeight: 25, paddingEnd: 36 },\n});\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/modal-layer.tsx"
    }
  ],
  "type": "registry:ui"
}