{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-select",
  "title": "Glass Select",
  "description": "A glass trigger that opens a slab of glass to choose from.",
  "dependencies": [
    "react-native-safe-area-context",
    "react-native-svg"
  ],
  "registryDependencies": [
    "@glassyui-native/glass-provider",
    "@glassyui-native/glass-surface"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-select.tsx",
      "content": "import {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useState,\n  type ReactNode,\n} from 'react';\nimport {\n  Animated,\n  Pressable,\n  StyleSheet,\n  Text,\n  View,\n  type GestureResponderEvent,\n  type PressableProps,\n  type StyleProp,\n  type TextProps,\n  type ViewProps,\n  type ViewStyle,\n} from 'react-native';\nimport { useGlassTheme } from './glass-provider';\nimport { GlassSurface } from './glass-surface';\nimport {\n  AnchoredContent,\n  OverlayItem,\n  OverlayLabel,\n  OverlaySeparator,\n  assignRef,\n  type OverlayItemProps,\n  type OverlayPlacementProps,\n} from './lib/anchored-overlay';\nimport { GlassIcon } from './lib/icons';\nimport { plainTextOf } from './lib/plain-text';\nimport { GLASS_MOTION, GLASS_RADIUS } from './lib/tokens';\nimport { useControllableState } from './lib/use-controllable-state';\n\ntype SelectItemLabel = {\n  node: ReactNode;\n  /** The label as plain text, for screen readers; undefined when it holds elements. */\n  text: string | undefined;\n};\n\ntype SelectContextValue = {\n  anchor: View | null;\n  disabled: boolean;\n  labels: ReadonlyMap<string, SelectItemLabel>;\n  open: boolean;\n  registerItem: (value: string, label: SelectItemLabel) => () => void;\n  selectValue: (value: string) => void;\n  setAnchor: (node: View | null) => void;\n  setOpen: (open: boolean) => void;\n  /** An empty string means nothing is chosen, as on the web. */\n  value: string;\n};\n\nconst SelectContext = createContext<SelectContextValue | null>(null);\n\nfunction useSelectContext(component: string) {\n  const context = useContext(SelectContext);\n\n  if (!context) throw new Error(`${component} must be used within GlassSelect`);\n\n  return context;\n}\n\n// The web's lg-select-content: 1.25rem corners, with options set at 0.9375rem.\nconst SELECT_RADIUS = 20;\n\nexport type GlassSelectProps = {\n  children?: ReactNode;\n  defaultOpen?: boolean;\n  defaultValue?: string;\n  disabled?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onValueChange?: (value: string) => void;\n  open?: boolean;\n  value?: string;\n};\n\n/** Holds a select's value and open state for its trigger and list. */\nexport function GlassSelect({\n  children,\n  defaultOpen = false,\n  defaultValue = '',\n  disabled = false,\n  onOpenChange,\n  onValueChange,\n  open: openProp,\n  value: valueProp,\n}: GlassSelectProps) {\n  const [open, setOpen] = useControllableState(openProp, defaultOpen, onOpenChange);\n  const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange);\n  const [anchor, setAnchor] = useState<View | null>(null);\n  const [labels, setLabels] = useState<ReadonlyMap<string, SelectItemLabel>>(() => new Map());\n\n  // Items report their labels, so the trigger can show the chosen one before the list opens.\n  const registerItem = useCallback((itemValue: string, label: SelectItemLabel) => {\n    setLabels((previous) => {\n      const current = previous.get(itemValue);\n\n      if (current && current.node === label.node && current.text === label.text) return previous;\n\n      return new Map(previous).set(itemValue, label);\n    });\n\n    return () =>\n      setLabels((previous) => {\n        if (!previous.has(itemValue)) return previous;\n\n        const next = new Map(previous);\n\n        next.delete(itemValue);\n\n        return next;\n      });\n  }, []);\n\n  const selectValue = useCallback(\n    (next: string) => {\n      if (next !== value) setValue(next);\n\n      setOpen(false);\n    },\n    [setOpen, setValue, value],\n  );\n\n  const context = useMemo(\n    () => ({\n      anchor,\n      disabled,\n      labels,\n      open,\n      registerItem,\n      selectValue,\n      setAnchor,\n      setOpen,\n      value,\n    }),\n    [anchor, disabled, labels, open, registerItem, selectValue, setOpen, value],\n  );\n\n  return <SelectContext.Provider value={context}>{children}</SelectContext.Provider>;\n}\n\nexport type GlassSelectTriggerProps = Omit<PressableProps, 'children' | 'style'> & {\n  children?: ReactNode;\n  style?: StyleProp<ViewStyle>;\n};\n\n/** The closed select: a glass capsule with the chosen option and a chevron that turns as it opens. */\nexport const GlassSelectTrigger = forwardRef<View, GlassSelectTriggerProps>(\n  ({ children, disabled: disabledProp, onPress, style, ...rest }, forwardedRef) => {\n    const { colors, preferences } = useGlassTheme();\n    const select = useSelectContext('GlassSelectTrigger');\n    const { open, setAnchor, setOpen } = select;\n    const disabled = select.disabled || Boolean(disabledProp);\n    const [turn] = useState(() => new Animated.Value(open ? 1 : 0));\n    const spoken = select.value === '' ? undefined : select.labels.get(select.value)?.text;\n\n    useEffect(() => {\n      if (preferences.reduceMotion) {\n        turn.setValue(open ? 1 : 0);\n\n        return;\n      }\n\n      Animated.spring(turn, {\n        ...GLASS_MOTION.spring,\n        toValue: open ? 1 : 0,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }).start();\n    }, [open, preferences.reduceMotion, turn]);\n\n    const ref = useCallback(\n      (node: View | null) => {\n        setAnchor(node);\n        assignRef(forwardedRef, node);\n      },\n      [forwardedRef, setAnchor],\n    );\n\n    const handlePress = useCallback(\n      (event: GestureResponderEvent) => {\n        onPress?.(event);\n\n        if (!event.isDefaultPrevented()) setOpen(!open);\n      },\n      [onPress, open, setOpen],\n    );\n\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"button\"\n        accessibilityState={{ expanded: open }}\n        accessibilityValue={spoken === undefined ? undefined : { text: spoken }}\n        disabled={disabled}\n        onPress={handlePress}\n        style={[disabled && styles.disabled, style]}\n        {...rest}\n      >\n        <GlassSurface elevation=\"none\" preset=\"control\" style={styles.trigger}>\n          {children}\n          <Animated.View\n            style={{\n              transform: [\n                {\n                  rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }),\n                },\n              ],\n            }}\n          >\n            <GlassIcon color={colors.mutedForeground} name=\"chevron-down\" size={16} />\n          </Animated.View>\n        </GlassSurface>\n      </Pressable>\n    );\n  },\n);\n\nGlassSelectTrigger.displayName = 'GlassSelectTrigger';\n\nexport type GlassSelectValueProps = TextProps & {\n  /** Shown in muted ink while nothing is chosen. */\n  placeholder?: ReactNode;\n};\n\n/** The chosen option's label inside the trigger, or the placeholder while nothing is chosen. */\nexport const GlassSelectValue = forwardRef<Text, GlassSelectValueProps>(\n  ({ children, placeholder, style, ...rest }, ref) => {\n    const { colors } = useGlassTheme();\n    const select = useSelectContext('GlassSelectValue');\n    const empty = select.value === '';\n\n    return (\n      <Text\n        ref={ref}\n        numberOfLines={1}\n        style={[styles.value, { color: empty ? colors.mutedForeground : colors.foreground }, style]}\n        {...rest}\n      >\n        {empty ? placeholder : (children ?? select.labels.get(select.value)?.node)}\n      </Text>\n    );\n  },\n);\n\nGlassSelectValue.displayName = 'GlassSelectValue';\n\nexport type GlassSelectContentProps = Omit<ViewProps, 'children'> &\n  OverlayPlacementProps & {\n    children?: ReactNode;\n  };\n\n/** The open list: a glass slab under the trigger, at least as wide as the trigger. */\nexport const GlassSelectContent = forwardRef<View, GlassSelectContentProps>(\n  ({ align = 'start', children, ...rest }, ref) => {\n    const { anchor, open, setOpen } = useSelectContext('GlassSelectContent');\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        keepMounted\n        minWidth=\"trigger\"\n        onClose={close}\n        open={open}\n        padding={6}\n        radius={SELECT_RADIUS}\n        {...rest}\n      >\n        {children}\n      </AnchoredContent>\n    );\n  },\n);\n\nGlassSelectContent.displayName = 'GlassSelectContent';\n\nexport type GlassSelectItemProps = Omit<\n  OverlayItemProps,\n  'indicator' | 'inset' | 'onPress' | 'textStyle' | 'tone'\n> & {\n  /** The label as plain text, for screen readers, when the label holds elements. */\n  textValue?: string;\n  /** Must not be empty: an empty value means nothing is chosen. */\n  value: string;\n};\n\n/** An option in the list. The chosen one shows a check; choosing one closes the list. */\nexport const GlassSelectItem = forwardRef<View, GlassSelectItemProps>(\n  ({ children, textValue, value, ...rest }, ref) => {\n    const { colors } = useGlassTheme();\n    const select = useSelectContext('GlassSelectItem');\n    const { registerItem, selectValue } = select;\n    const selected = select.value === value;\n    const text = textValue ?? plainTextOf(children);\n\n    useLayoutEffect(\n      () => registerItem(value, { node: children, text }),\n      [children, registerItem, text, value],\n    );\n\n    const handlePress = useCallback(() => selectValue(value), [selectValue, value]);\n\n    return (\n      <OverlayItem\n        ref={ref}\n        accessibilityState={{ selected }}\n        indicator={selected ? <GlassIcon color={colors.foreground} name=\"check\" size={16} /> : null}\n        inset\n        onPress={handlePress}\n        textStyle={styles.itemText}\n        {...rest}\n      >\n        {children}\n      </OverlayItem>\n    );\n  },\n);\n\nGlassSelectItem.displayName = 'GlassSelectItem';\n\n/** Groups related options. */\nexport const GlassSelectGroup = forwardRef<View, ViewProps>((props, ref) => (\n  <View ref={ref} {...props} />\n));\n\nGlassSelectGroup.displayName = 'GlassSelectGroup';\n\n/** A small muted heading over a group of options. */\nexport const GlassSelectLabel = forwardRef<Text, TextProps>((props, ref) => (\n  <OverlayLabel ref={ref} {...props} />\n));\n\nGlassSelectLabel.displayName = 'GlassSelectLabel';\n\n/** A hairline between groups of options. */\nexport const GlassSelectSeparator = forwardRef<View, ViewProps>((props, ref) => (\n  <OverlaySeparator ref={ref} {...props} />\n));\n\nGlassSelectSeparator.displayName = 'GlassSelectSeparator';\n\n// The trigger follows the web's lg-input field as a capsule; options follow lg-select-content.\nconst styles = StyleSheet.create({\n  disabled: { opacity: 0.45 },\n  itemText: { fontSize: 15, lineHeight: 21 },\n  trigger: {\n    alignItems: 'center',\n    borderRadius: GLASS_RADIUS.capsule,\n    flexDirection: 'row',\n    gap: 12,\n    justifyContent: 'space-between',\n    minHeight: 44,\n    paddingHorizontal: 20,\n    paddingVertical: 12,\n  },\n  value: { flexShrink: 1, fontSize: 16, lineHeight: 22 },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-select.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/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"
    },
    {
      "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"
}