{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-menu",
  "title": "Glass Menu",
  "description": "A menu on glass that grows from its trigger, with checkbox and radio items.",
  "dependencies": [
    "react-native-safe-area-context",
    "react-native-svg"
  ],
  "registryDependencies": [
    "@glassyui-native/glass-provider",
    "@glassyui-native/glass-surface"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-menu.tsx",
      "content": "import {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n  type ReactElement,\n  type ReactNode,\n} from 'react';\nimport { StyleSheet, Text, View, type TextProps, type ViewProps } from 'react-native';\nimport { useGlassTheme } from './glass-provider';\nimport {\n  AnchoredContent,\n  OverlayItem,\n  OverlayLabel,\n  OverlaySeparator,\n  OverlayTrigger,\n  type OverlayItemProps,\n  type OverlayPlacementProps,\n  type OverlayTriggerChildProps,\n} from './lib/anchored-overlay';\nimport { GlassIcon } from './lib/icons';\nimport { useControllableState } from './lib/use-controllable-state';\n\ntype MenuContextValue = {\n  anchor: View | null;\n  open: boolean;\n  setAnchor: (node: View | null) => void;\n  setOpen: (open: boolean) => void;\n};\n\nconst MenuContext = createContext<MenuContextValue | null>(null);\n\nfunction useMenuContext(component: string) {\n  const context = useContext(MenuContext);\n\n  if (!context) throw new Error(`${component} must be used within GlassMenu`);\n\n  return context;\n}\n\ntype RadioGroupContextValue = {\n  onValueChange?: (value: string) => void;\n  value?: string;\n};\n\nconst RadioGroupContext = createContext<RadioGroupContextValue | null>(null);\n\nfunction useRadioGroupContext() {\n  const context = useContext(RadioGroupContext);\n\n  if (!context) throw new Error('GlassMenuRadioItem must be used within GlassMenuRadioGroup');\n\n  return context;\n}\n\n// The web's lg-menu-content: 14rem wide at least, with 1.125rem corners.\nconst MENU_MIN_WIDTH = 224;\n\nconst MENU_RADIUS = 18;\n\nexport type GlassMenuProps = {\n  children?: ReactNode;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  open?: boolean;\n};\n\n/** Holds a menu's open state for its trigger and content. */\nexport function GlassMenu({ children, defaultOpen = false, onOpenChange, open }: GlassMenuProps) {\n  const [isOpen, setOpen] = useControllableState(open, defaultOpen, onOpenChange);\n  const [anchor, setAnchor] = useState<View | null>(null);\n\n  const value = useMemo(\n    () => ({ anchor, open: isOpen, setAnchor, setOpen }),\n    [anchor, isOpen, setOpen],\n  );\n\n  return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>;\n}\n\nexport type GlassMenuTriggerProps = {\n  /** One pressable element, such as a GlassButton, that passes its ref to a view. */\n  children: ReactElement<OverlayTriggerChildProps>;\n};\n\n/** Opens the menu from its one child, such as a GlassButton, and marks that child expanded. */\nexport function GlassMenuTrigger({ children }: GlassMenuTriggerProps) {\n  const menu = useMenuContext('GlassMenuTrigger');\n\n  return (\n    <OverlayTrigger onAnchor={menu.setAnchor} onOpenChange={menu.setOpen} open={menu.open}>\n      {children}\n    </OverlayTrigger>\n  );\n}\n\nexport type GlassMenuContentProps = Omit<ViewProps, 'children'> &\n  OverlayPlacementProps & {\n    children?: ReactNode;\n  };\n\n/** The open menu: a glass slab that grows out of its trigger and scrolls when it runs out of room. */\nexport const GlassMenuContent = forwardRef<View, GlassMenuContentProps>(\n  ({ align = 'center', children, ...rest }, ref) => {\n    const { anchor, open, setOpen } = useMenuContext('GlassMenuContent');\n    const close = useCallback(() => setOpen(false), [setOpen]);\n\n    return (\n      <AnchoredContent\n        ref={ref}\n        accessibilityRole=\"menu\"\n        align={align}\n        anchor={anchor}\n        minWidth={MENU_MIN_WIDTH}\n        onClose={close}\n        open={open}\n        padding={6}\n        radius={MENU_RADIUS}\n        {...rest}\n      >\n        {children}\n      </AnchoredContent>\n    );\n  },\n);\n\nGlassMenuContent.displayName = 'GlassMenuContent';\n\nexport type GlassMenuSelectEvent = {\n  defaultPrevented: boolean;\n  /** Keeps the menu open after this selection. */\n  preventDefault: () => void;\n};\n\nfunction createSelectEvent(): GlassMenuSelectEvent {\n  const event = {\n    defaultPrevented: false,\n    preventDefault: () => {\n      event.defaultPrevented = true;\n    },\n  };\n\n  return event;\n}\n\nexport type GlassMenuItemProps = Omit<OverlayItemProps, 'indicator' | 'onPress' | 'textStyle'> & {\n  /** Runs when the row is chosen. The menu then closes unless the handler prevents it. */\n  onSelect?: (event: GlassMenuSelectEvent) => void;\n};\n\ntype MenuRowProps = GlassMenuItemProps & {\n  indicator?: ReactNode;\n};\n\nconst MenuRow = forwardRef<View, MenuRowProps>(({ onSelect, ...rest }, ref) => {\n  const { setOpen } = useMenuContext('GlassMenuItem');\n\n  const handlePress = useCallback(() => {\n    const event = createSelectEvent();\n\n    onSelect?.(event);\n\n    if (!event.defaultPrevented) setOpen(false);\n  }, [onSelect, setOpen]);\n\n  return <OverlayItem ref={ref} onPress={handlePress} {...rest} />;\n});\n\nMenuRow.displayName = 'MenuRow';\n\n/** A menu row that runs `onSelect` and closes the menu. */\nexport const GlassMenuItem = forwardRef<View, GlassMenuItemProps>((props, ref) => (\n  <MenuRow ref={ref} {...props} />\n));\n\nGlassMenuItem.displayName = 'GlassMenuItem';\n\nexport type GlassMenuCheckboxItemProps = Omit<GlassMenuItemProps, 'inset' | 'tone'> & {\n  checked?: boolean | 'indeterminate';\n  onCheckedChange?: (checked: boolean) => void;\n};\n\n/** A menu row that toggles a setting and shows a check while it is on. */\nexport const GlassMenuCheckboxItem = forwardRef<View, GlassMenuCheckboxItemProps>(\n  ({ checked = false, onCheckedChange, onSelect, ...rest }, ref) => {\n    const { colors } = useGlassTheme();\n    const mixed = checked === 'indeterminate';\n\n    const handleSelect = useCallback(\n      (event: GlassMenuSelectEvent) => {\n        onSelect?.(event);\n        onCheckedChange?.(mixed ? true : !checked);\n      },\n      [checked, mixed, onCheckedChange, onSelect],\n    );\n\n    return (\n      <MenuRow\n        ref={ref}\n        accessibilityState={{ checked: mixed ? 'mixed' : checked }}\n        indicator={\n          checked ? (\n            <GlassIcon color={colors.foreground} name={mixed ? 'dash' : 'check'} size={16} />\n          ) : null\n        }\n        inset\n        onSelect={handleSelect}\n        {...rest}\n      />\n    );\n  },\n);\n\nGlassMenuCheckboxItem.displayName = 'GlassMenuCheckboxItem';\n\nexport type GlassMenuRadioGroupProps = ViewProps & {\n  onValueChange?: (value: string) => void;\n  value?: string;\n};\n\n/** Groups radio rows; the row whose value matches `value` shows a dot. */\nexport const GlassMenuRadioGroup = forwardRef<View, GlassMenuRadioGroupProps>(\n  ({ onValueChange, value, ...rest }, ref) => {\n    const group = useMemo(() => ({ onValueChange, value }), [onValueChange, value]);\n\n    return (\n      <RadioGroupContext.Provider value={group}>\n        <View ref={ref} accessibilityRole=\"radiogroup\" {...rest} />\n      </RadioGroupContext.Provider>\n    );\n  },\n);\n\nGlassMenuRadioGroup.displayName = 'GlassMenuRadioGroup';\n\nexport type GlassMenuRadioItemProps = Omit<GlassMenuItemProps, 'inset' | 'tone'> & {\n  value: string;\n};\n\n/** A menu row that chooses one value from its radio group. */\nexport const GlassMenuRadioItem = forwardRef<View, GlassMenuRadioItemProps>(\n  ({ onSelect, value, ...rest }, ref) => {\n    const { colors } = useGlassTheme();\n    const group = useRadioGroupContext();\n    const { onValueChange } = group;\n    const checked = group.value === value;\n\n    const handleSelect = useCallback(\n      (event: GlassMenuSelectEvent) => {\n        onSelect?.(event);\n        onValueChange?.(value);\n      },\n      [onSelect, onValueChange, value],\n    );\n\n    return (\n      <MenuRow\n        ref={ref}\n        accessibilityState={{ checked }}\n        indicator={\n          checked ? <View style={[styles.dot, { backgroundColor: colors.foreground }]} /> : null\n        }\n        inset\n        onSelect={handleSelect}\n        {...rest}\n      />\n    );\n  },\n);\n\nGlassMenuRadioItem.displayName = 'GlassMenuRadioItem';\n\n/** A small muted heading over a group of menu rows. */\nexport const GlassMenuLabel = forwardRef<Text, TextProps>((props, ref) => (\n  <OverlayLabel ref={ref} {...props} />\n));\n\nGlassMenuLabel.displayName = 'GlassMenuLabel';\n\n/** A hairline between groups of menu rows. */\nexport const GlassMenuSeparator = forwardRef<View, ViewProps>((props, ref) => (\n  <OverlaySeparator ref={ref} {...props} />\n));\n\nGlassMenuSeparator.displayName = 'GlassMenuSeparator';\n\n/** Groups related menu rows. */\nexport const GlassMenuGroup = forwardRef<View, ViewProps>((props, ref) => (\n  <View ref={ref} {...props} />\n));\n\nGlassMenuGroup.displayName = 'GlassMenuGroup';\n\n/** Trailing muted text on a menu row, such as a keyboard shortcut. */\nexport const GlassMenuShortcut = forwardRef<Text, TextProps>(({ style, ...rest }, ref) => {\n  const { colors } = useGlassTheme();\n\n  return (\n    <Text ref={ref} style={[styles.shortcut, { color: colors.mutedForeground }, style]} {...rest} />\n  );\n});\n\nGlassMenuShortcut.displayName = 'GlassMenuShortcut';\n\n// The web's lg-menu-indicator dot and lg-menu-shortcut.\nconst styles = StyleSheet.create({\n  dot: { borderRadius: 3.25, height: 6.5, width: 6.5 },\n  shortcut: { fontSize: 12, letterSpacing: 0.72, marginStart: 'auto', paddingStart: 24 },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-menu.tsx"
    },
    {
      "path": "registry/glassyui-native/lib/anchored-overlay.tsx",
      "content": "import {\n  cloneElement,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useState,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n} from 'react';\nimport {\n  Animated,\n  I18nManager,\n  Pressable,\n  ScrollView,\n  StyleSheet,\n  Text,\n  View,\n  useWindowDimensions,\n  type AccessibilityState,\n  type GestureResponderEvent,\n  type LayoutChangeEvent,\n  type PressableProps,\n  type StyleProp,\n  type TextProps,\n  type TextStyle,\n  type ViewProps,\n  type ViewStyle,\n} from 'react-native';\nimport { initialWindowMetrics, SafeAreaInsetsContext } from 'react-native-safe-area-context';\nimport { useGlassTheme } from '../glass-provider';\nimport { GlassSurface } from '../glass-surface';\nimport { LAYER_EASING, ModalLayerHost, useLayerPresence } from './modal-layer';\nimport { wrapPlainText } from './plain-text';\nimport { GLASS_MOTION } from './tokens';\n\nexport type OverlaySide = 'top' | 'bottom';\n\nexport type OverlayAlign = 'start' | 'center' | 'end';\n\nexport type OverlayPlacementProps = {\n  /** The trigger edge the layer lines up with. Start and end follow the reading direction. */\n  align?: OverlayAlign;\n  /** The side of the trigger the layer opens on. It flips when the other side has more room. */\n  side?: OverlaySide;\n  /** The gap between the trigger and the layer, in points. Defaults to 6. */\n  sideOffset?: number;\n};\n\ntype Rect = { height: number; width: number; x: number; y: number };\n\ntype Size = { height: number; width: number };\n\ntype Bounds = { bottom: number; left: number; right: number; top: number };\n\ntype Placement = {\n  /** The tallest the layer may be on its side; beyond that it scrolls. */\n  maxHeight: number;\n  /** Where the trigger's centre falls across the layer, the point it grows from. */\n  originX: number;\n  side: OverlaySide;\n  /** The layer's left edge and top edge in window coordinates. */\n  x: number;\n  y: number;\n};\n\n// Layers keep this far inside the window and its safe areas.\nconst EDGE_MARGIN = 8;\n\nconst DEFAULT_SIDE_OFFSET = 6;\n\n// A layer grows out of its trigger from 94% and drifts its last few points away from it.\nconst ENTER_SCALE = 0.94;\n\nconst DRIFT = 4;\n\n// The web's lg-pop-in and lg-pop-out: arrivals ease out, exits ease in and run shorter.\nconst ENTER = { duration: 240, easing: LAYER_EASING.out };\n\nconst EXIT = { duration: 160, easing: LAYER_EASING.in };\n\nconst MIRRORED_ALIGN = { center: 'center', end: 'start', start: 'end' } as const satisfies Record<\n  OverlayAlign,\n  OverlayAlign\n>;\n\nconst NO_INSETS = { bottom: 0, left: 0, right: 0, top: 0 };\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(Math.max(value, min), Math.max(min, max));\n}\n\n/** Where a layer of `size` sits beside `anchor`, kept inside `bounds`. All in window points. */\nfunction placeLayer(\n  anchor: Rect,\n  size: Size,\n  bounds: Bounds,\n  preferred: OverlaySide,\n  align: OverlayAlign,\n  sideOffset: number,\n): Placement {\n  const below = anchor.y + anchor.height + sideOffset;\n  const above = anchor.y - sideOffset;\n  const room = { bottom: bounds.bottom - below, top: above - bounds.top };\n  const other = preferred === 'bottom' ? 'top' : 'bottom';\n  const side = size.height > room[preferred] && room[other] > room[preferred] ? other : preferred;\n  const maxHeight = Math.max(room[side], 0);\n  const height = Math.min(size.height, maxHeight);\n  const y = clamp(side === 'bottom' ? below : above - height, bounds.top, bounds.bottom - height);\n  const edge = I18nManager.isRTL ? MIRRORED_ALIGN[align] : align;\n\n  const offset = {\n    center: (anchor.width - size.width) / 2,\n    end: anchor.width - size.width,\n    start: 0,\n  };\n\n  const x = clamp(anchor.x + offset[edge], bounds.left, bounds.right - size.width);\n\n  return {\n    maxHeight,\n    originX: clamp(anchor.x + anchor.width / 2 - x, 0, size.width),\n    side,\n    x,\n    y,\n  };\n}\n\n/**\n * Puts a placed layer in position and grows it out of its trigger as `progress` runs from 0 to 1.\n * Yoga reads start and end by reading direction, so in right-to-left layouts end is the left edge.\n */\nfunction layerMotion(placement: Placement, progress: Animated.Value) {\n  const drift = placement.side === 'top' ? DRIFT : -DRIFT;\n  const originY = placement.side === 'top' ? '100%' : '0px';\n\n  return [\n    I18nManager.isRTL ? { end: placement.x } : { start: placement.x },\n    {\n      top: placement.y,\n      transform: [\n        { translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [drift, 0] }) },\n        { scale: progress.interpolate({ inputRange: [0, 1], outputRange: [ENTER_SCALE, 1] }) },\n      ],\n      // React Native reads whole points here, so the origin is rounded.\n      transformOrigin: `${Math.round(placement.originX)}px ${originY}`,\n    },\n  ];\n}\n\n/** Points `ref` at `node`, whether it is a callback or an object ref. */\nexport function assignRef<T>(ref: Ref<T> | undefined, node: T | null) {\n  if (ref instanceof Function) {\n    ref(node);\n\n    return;\n  }\n\n  if (ref) ref.current = node;\n}\n\nexport type OverlayTriggerChildProps = {\n  accessibilityState?: AccessibilityState;\n  onPress?: ((event: GestureResponderEvent) => void) | null;\n  ref?: Ref<View>;\n};\n\ntype OverlayTriggerProps = {\n  children: ReactElement<OverlayTriggerChildProps>;\n  /** Receives the child's view, which the layer measures to know where to open. */\n  onAnchor: (node: View | null) => void;\n  onOpenChange: (open: boolean) => void;\n  open: boolean;\n};\n\n/**\n * Renders its one child with a press that toggles the layer and an expanded state. The child's\n * own press and ref keep working; the child must pass its ref to a view, which the layer measures.\n */\nexport function OverlayTrigger({ children, onAnchor, onOpenChange, open }: OverlayTriggerProps) {\n  const { accessibilityState, onPress, ref: forwarded } = children.props;\n\n  const ref = useCallback(\n    (node: View | null) => {\n      onAnchor(node);\n      assignRef(forwarded, node);\n    },\n    [forwarded, onAnchor],\n  );\n\n  const handlePress = useCallback(\n    (event: GestureResponderEvent) => {\n      onPress?.(event);\n\n      if (!event.isDefaultPrevented()) onOpenChange(!open);\n    },\n    [onOpenChange, onPress, open],\n  );\n\n  return cloneElement(children, {\n    accessibilityState: { ...accessibilityState, expanded: open },\n    onPress: handlePress,\n    ref,\n  });\n}\n\nexport type AnchoredContentProps = Omit<ViewProps, 'children' | 'style'> &\n  OverlayPlacementProps & {\n    /** The view the layer opens beside, usually its trigger. */\n    anchor: View | null;\n    children?: ReactNode;\n    /** Keeps the children mounted in a hidden, inert view while the layer is closed. */\n    keepMounted?: boolean;\n    /** The layer's narrowest width in points, or 'trigger' for at least the trigger's width. */\n    minWidth?: number | 'trigger';\n    onClose: () => void;\n    open: boolean;\n    /** Space between the glass edge and the content that scrolls inside it. */\n    padding: number;\n    radius: number;\n    /** Applies to the glass. */\n    style?: StyleProp<ViewStyle>;\n  };\n\ntype AnchoredLayerProps = Omit<AnchoredContentProps, 'keepMounted'> & {\n  onExited: () => void;\n};\n\nconst AnchoredLayer = forwardRef<View, AnchoredLayerProps>(\n  (\n    {\n      align = 'center',\n      anchor,\n      children,\n      minWidth = 0,\n      onClose,\n      onExited,\n      open,\n      padding,\n      radius,\n      side = 'bottom',\n      sideOffset = DEFAULT_SIDE_OFFSET,\n      style,\n      ...rest\n    },\n    ref,\n  ) => {\n    const { preferences } = useGlassTheme();\n    const viewport = useWindowDimensions();\n    const insets = useContext(SafeAreaInsetsContext) ?? initialWindowMetrics?.insets ?? NO_INSETS;\n    const [progress] = useState(() => new Animated.Value(0));\n    const [frame, setFrame] = useState<Rect | null>(null);\n    const [size, setSize] = useState<Size | null>(null);\n\n    const bounds = {\n      bottom: viewport.height - insets.bottom - EDGE_MARGIN,\n      left: insets.left + EDGE_MARGIN,\n      right: viewport.width - insets.right - EDGE_MARGIN,\n      top: insets.top + EDGE_MARGIN,\n    };\n\n    const widest = bounds.right - bounds.left;\n    const narrowest = Math.min(minWidth === 'trigger' ? (frame?.width ?? 0) : minWidth, widest);\n\n    // Find the trigger when the layer opens, and again when the window turns or resizes.\n    useLayoutEffect(() => {\n      anchor?.measureInWindow((x, y, width, height) => setFrame({ height, width, x, y }));\n    }, [anchor, viewport.height, viewport.width]);\n\n    // A layer that opens with its trigger gets the trigger's view a render later, so only an\n    // anchor still missing on the next frame is a mistake.\n    useEffect(() => {\n      if (anchor) return;\n\n      const frameId = requestAnimationFrame(() =>\n        console.error(\n          'Glassy UI: a menu, select or popover opened without a trigger to anchor to. Its trigger child must pass its ref to a view.',\n        ),\n      );\n\n      return () => cancelAnimationFrame(frameId);\n    }, [anchor]);\n\n    const handleLayout = useCallback((event: LayoutChangeEvent) => {\n      const { height, width } = event.nativeEvent.layout;\n\n      setSize((previous) =>\n        previous?.height === height && previous.width === width ? previous : { height, width },\n      );\n    }, []);\n\n    // A size measured before the trigger's width applied is stale; the next layout replaces it.\n    const placement =\n      frame && size && size.width >= narrowest - 0.5\n        ? placeLayer(frame, size, bounds, side, align, sideOffset)\n        : null;\n\n    const placed = placement !== null;\n\n    useEffect(() => {\n      if (!placed) {\n        if (!open) onExited();\n\n        return;\n      }\n\n      if (preferences.reduceMotion) {\n        progress.setValue(open ? 1 : 0);\n\n        if (!open) onExited();\n\n        return;\n      }\n\n      const motion = open ? ENTER : EXIT;\n\n      const animation = Animated.timing(progress, {\n        duration: motion.duration,\n        easing: motion.easing,\n        toValue: open ? 1 : 0,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      });\n\n      animation.start(({ finished }) => {\n        if (finished && !open) onExited();\n      });\n\n      return () => animation.stop();\n    }, [onExited, open, placed, preferences.reduceMotion, progress]);\n\n    return (\n      <>\n        <Pressable\n          accessible={false}\n          importantForAccessibility=\"no\"\n          onPress={onClose}\n          style={StyleSheet.absoluteFill}\n        />\n        <Animated.View\n          onLayout={handleLayout}\n          pointerEvents={placed ? 'auto' : 'none'}\n          style={[styles.layer, placement ? layerMotion(placement, progress) : styles.measuring]}\n        >\n          <GlassSurface\n            ref={ref}\n            elevation=\"raised\"\n            preset=\"overlay\"\n            style={[\n              {\n                borderRadius: radius,\n                maxHeight: placement?.maxHeight ?? bounds.bottom - bounds.top,\n                maxWidth: widest,\n                minWidth: narrowest,\n              },\n              style,\n            ]}\n            {...rest}\n            accessibilityViewIsModal\n            onAccessibilityEscape={onClose}\n          >\n            <ScrollView\n              alwaysBounceVertical={false}\n              contentContainerStyle={{ padding }}\n              keyboardShouldPersistTaps=\"handled\"\n              style={{\n                borderCurve: 'continuous',\n                borderRadius: StyleSheet.flatten(style)?.borderRadius ?? radius,\n              }}\n            >\n              {children}\n            </ScrollView>\n          </GlassSurface>\n        </Animated.View>\n      </>\n    );\n  },\n);\n\nAnchoredLayer.displayName = 'AnchoredLayer';\n\n/**\n * A glass layer anchored to a trigger. It opens in a transparent modal, measures itself before it\n * shows, grows out of the trigger and closes on an outside tap or the Android back button. Glass\n * never fades: it scales and drifts, and Reduce Motion shows and hides it at once.\n */\nexport const AnchoredContent = forwardRef<View, AnchoredContentProps>(\n  ({ children, keepMounted = false, onClose, open, ...rest }, ref) => {\n    const { preferences } = useGlassTheme();\n    const [present, hide] = useLayerPresence(open, preferences.reduceMotion);\n\n    return (\n      <>\n        <ModalLayerHost onRequestClose={onClose} present={present}>\n          <AnchoredLayer ref={ref} onClose={onClose} onExited={hide} open={open} {...rest}>\n            {children}\n          </AnchoredLayer>\n        </ModalLayerHost>\n        {keepMounted && !present ? (\n          <View\n            accessibilityElementsHidden\n            importantForAccessibility=\"no-hide-descendants\"\n            style={[{ pointerEvents: 'none' }, styles.hidden]}\n          >\n            {children}\n          </View>\n        ) : null}\n      </>\n    );\n  },\n);\n\nAnchoredContent.displayName = 'AnchoredContent';\n\nexport type OverlayItemProps = Omit<PressableProps, 'children' | 'style'> & {\n  children?: ReactNode;\n  /** A check or dot drawn in the inset at the row's start. */\n  indicator?: ReactNode;\n  /** Leaves room for an indicator at the start, so labels line up. */\n  inset?: boolean;\n  style?: StyleProp<ViewStyle>;\n  textStyle?: StyleProp<TextStyle>;\n  tone?: 'default' | 'destructive';\n};\n\n// A pressed row lightens dark glass and darkens light glass, as .lg-menu-item[data-highlighted].\nconst PRESSED_ROW = { dark: 'rgba(255, 255, 255, 0.14)', light: 'rgba(0, 0, 0, 0.06)' } as const;\n\n/** A 44-point row in a menu or a select list; a press tints the row under it. */\nexport const OverlayItem = forwardRef<View, OverlayItemProps>(\n  (\n    {\n      children,\n      disabled = false,\n      indicator,\n      inset = false,\n      style,\n      textStyle,\n      tone = 'default',\n      ...rest\n    },\n    ref,\n  ) => {\n    const { colors, preferences } = useGlassTheme();\n    const ink = tone === 'destructive' ? colors.invalid : colors.foreground;\n\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"menuitem\"\n        disabled={disabled}\n        style={({ pressed }) => [\n          styles.item,\n          inset && styles.inset,\n          pressed && { backgroundColor: PRESSED_ROW[preferences.theme] },\n          disabled && styles.disabled,\n          style,\n        ]}\n        {...rest}\n      >\n        {indicator ? <View style={styles.indicator}>{indicator}</View> : null}\n        {wrapPlainText(children, (text) => (\n          <Text style={[styles.itemText, { color: ink }, textStyle]}>{text}</Text>\n        ))}\n      </Pressable>\n    );\n  },\n);\n\nOverlayItem.displayName = 'OverlayItem';\n\n/** A small muted heading over a group of rows. */\nexport const OverlayLabel = forwardRef<Text, TextProps>(({ style, ...rest }, ref) => {\n  const { colors } = useGlassTheme();\n\n  return (\n    <Text\n      ref={ref}\n      accessibilityRole=\"header\"\n      style={[styles.label, { color: colors.mutedForeground }, style]}\n      {...rest}\n    />\n  );\n});\n\nOverlayLabel.displayName = 'OverlayLabel';\n\n/** A hairline between groups of rows. Screen readers skip it. */\nexport const OverlaySeparator = forwardRef<View, ViewProps>(({ style, ...rest }, ref) => {\n  const { colors } = useGlassTheme();\n\n  return (\n    <View\n      ref={ref}\n      accessibilityElementsHidden\n      importantForAccessibility=\"no\"\n      style={[styles.separator, { backgroundColor: colors.border }, style]}\n      {...rest}\n    />\n  );\n});\n\nOverlaySeparator.displayName = 'OverlaySeparator';\n\n// Rows, labels and separators copy the web's lg-menu-item, lg-menu-label and lg-menu-separator.\n// Rows grow from the web's 36 pixels to a 44-point touch target.\nconst styles = StyleSheet.create({\n  disabled: { opacity: 0.45 },\n  hidden: { display: 'none' },\n  indicator: {\n    alignItems: 'center',\n    bottom: 0,\n    justifyContent: 'center',\n    position: 'absolute',\n    start: 8,\n    top: 0,\n    width: 16,\n  },\n  inset: { paddingStart: 32 },\n  item: {\n    alignItems: 'center',\n    borderCurve: 'continuous',\n    borderRadius: 12,\n    flexDirection: 'row',\n    gap: 10,\n    minHeight: 44,\n    paddingHorizontal: 12,\n    paddingVertical: 7,\n  },\n  itemText: { flexShrink: 1, fontSize: 14, lineHeight: 20 },\n  label: {\n    fontSize: 12,\n    fontWeight: '600',\n    lineHeight: 16,\n    paddingBottom: 4,\n    paddingHorizontal: 12,\n    paddingTop: 6,\n  },\n  layer: { position: 'absolute' },\n  // Until it is measured, a layer waits off screen so it never shows in the wrong place.\n  measuring: { start: 0, top: -10000 },\n  separator: { height: 1, marginHorizontal: 10, marginVertical: 6 },\n});\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/anchored-overlay.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"
    },
    {
      "path": "registry/glassyui-native/lib/plain-text.ts",
      "content": "import { Children, isValidElement, type ReactNode } from 'react';\n\n/**\n * Sets plain text inside a Text, which React Native needs to render it. Content that is all text\n * becomes one Text; beside elements such as icons, each run of text gets its own.\n */\nexport function wrapPlainText(node: ReactNode, render: (text: ReactNode) => ReactNode) {\n  const parts = Children.toArray(node);\n\n  if (parts.length === 0) return null;\n\n  if (parts.every((part) => !isValidElement(part))) return render(node);\n\n  return Children.map(node, (part) => (isValidElement(part) ? part : render(part)));\n}\n\n/** The words in `node` when it is only text, to name a control for assistive technology. */\nexport function plainTextOf(node: ReactNode) {\n  const parts = Children.toArray(node);\n\n  return parts.length > 0 && parts.every((part) => !isValidElement(part))\n    ? parts.join('')\n    : undefined;\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/plain-text.ts"
    }
  ],
  "type": "registry:ui"
}