{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-dialog",
  "title": "Glass Dialog",
  "description": "A modal on raised glass that rises into place and holds focus until it closes.",
  "dependencies": [
    "react-native-safe-area-context",
    "react-native-svg"
  ],
  "registryDependencies": [
    "@glassyui-native/glass-provider",
    "@glassyui-native/glass-surface"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-dialog.tsx",
      "content": "import {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useState,\n  type ForwardedRef,\n  type ReactNode,\n} from 'react';\nimport {\n  Animated,\n  StyleSheet,\n  useWindowDimensions,\n  View,\n  type Text,\n  type TextProps,\n  type ViewProps,\n} from 'react-native';\nimport { useGlassTheme } from './glass-provider';\nimport { GlassSurface } from './glass-surface';\nimport {\n  LAYER_EASING,\n  ModalLayerClose,\n  ModalLayerCloseButton,\n  ModalLayerDescription,\n  ModalLayerHost,\n  ModalLayerRoot,\n  ModalLayerScrim,\n  ModalLayerTitle,\n  ModalLayerTrigger,\n  useLayerPresence,\n  useModalLayer,\n  useSafeInsets,\n  type ModalLayerRootProps,\n  type ModalLayerSlotProps,\n} from './lib/modal-layer';\nimport { GLASS_MOTION, GLASS_RADIUS } from './lib/tokens';\n\nexport type GlassDialogProps = ModalLayerRootProps;\n\nexport type GlassDialogTriggerProps = ModalLayerSlotProps;\n\nexport type GlassDialogCloseProps = ModalLayerSlotProps;\n\nexport type GlassDialogContentProps = ViewProps & {\n  children?: ReactNode;\n  /** Names the close button for assistive tech. Defaults to \"Close dialog\". */\n  closeLabel?: string;\n};\n\n// The dialog rises 8 points into place as it grows from 96%; on exit it drops off the screen.\nconst ARRIVAL = { duration: 240, easing: LAYER_EASING.out };\n\nconst EXIT = { duration: 200, easing: LAYER_EASING.in };\n\n/** Holds a dialog's open state for its trigger, content and close controls. */\nexport function GlassDialog(props: GlassDialogProps) {\n  return <ModalLayerRoot {...props} />;\n}\n\n/** Opens the dialog when its single child, such as a GlassButton, is pressed. */\nexport function GlassDialogTrigger({ children }: GlassDialogTriggerProps) {\n  return (\n    <ModalLayerTrigger component=\"GlassDialogTrigger\" root=\"GlassDialog\">\n      {children}\n    </ModalLayerTrigger>\n  );\n}\n\n/** Closes the dialog when its single child is pressed. */\nexport function GlassDialogClose({ children }: GlassDialogCloseProps) {\n  return (\n    <ModalLayerClose component=\"GlassDialogClose\" root=\"GlassDialog\">\n      {children}\n    </ModalLayerClose>\n  );\n}\n\ntype DialogLayerProps = GlassDialogContentProps & {\n  onClose: () => void;\n  onHidden: () => void;\n  open: boolean;\n  panelRef: ForwardedRef<View>;\n};\n\n// Mounted while the dialog is on screen, so each opening starts from fresh values.\nfunction DialogLayer({\n  children,\n  closeLabel = 'Close dialog',\n  onClose,\n  onHidden,\n  open,\n  panelRef,\n  style,\n  ...rest\n}: DialogLayerProps) {\n  const { preferences } = useGlassTheme();\n  const insets = useSafeInsets();\n  const { height, width } = useWindowDimensions();\n  const [progress] = useState(() => new Animated.Value(0));\n  const [drop] = useState(() => new Animated.Value(0));\n  const reduceMotion = preferences.reduceMotion;\n\n  useEffect(() => {\n    if (reduceMotion) {\n      progress.setValue(1);\n      drop.setValue(0);\n\n      return;\n    }\n\n    const timing = open ? ARRIVAL : EXIT;\n\n    const animation = Animated.parallel([\n      Animated.timing(progress, {\n        ...timing,\n        toValue: open ? 1 : 0,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }),\n      Animated.timing(drop, {\n        ...timing,\n        toValue: open ? 0 : 1,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }),\n    ]);\n\n    animation.start(({ finished }) => {\n      if (finished && !open) onHidden();\n    });\n\n    return () => animation.stop();\n  }, [drop, onHidden, open, progress, reduceMotion]);\n\n  const rise = progress.interpolate({ inputRange: [0, 1], outputRange: [8, 0] });\n  const fall = drop.interpolate({ inputRange: [0, 1], outputRange: [0, height] });\n\n  return (\n    <>\n      <ModalLayerScrim onPress={onClose} progress={progress} />\n      <View\n        style={[\n          { pointerEvents: 'box-none' },\n          StyleSheet.absoluteFill,\n          styles.stage,\n          {\n            paddingBottom: insets.bottom + 16,\n            paddingEnd: insets.end + 16,\n            paddingStart: insets.start + 16,\n            paddingTop: insets.top + 16,\n          },\n        ]}\n      >\n        <Animated.View\n          style={[\n            styles.frame,\n            {\n              transform: [\n                { translateY: Animated.add(rise, fall) },\n                { scale: progress.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) },\n              ],\n            },\n          ]}\n        >\n          <GlassSurface\n            ref={panelRef}\n            accessibilityViewIsModal\n            elevation=\"raised\"\n            onAccessibilityEscape={onClose}\n            preset=\"overlay\"\n            style={[styles.panel, { padding: width >= 640 ? 32 : 24 }, style]}\n            {...rest}\n          >\n            {children}\n            <ModalLayerCloseButton label={closeLabel} onPress={onClose} style={styles.close} />\n          </GlassSurface>\n        </Animated.View>\n      </View>\n    </>\n  );\n}\n\n/** A modal glass panel that rises over a scrim, with a close button in its corner. */\nexport const GlassDialogContent = forwardRef<View, GlassDialogContentProps>((props, ref) => {\n  const { open, setOpen } = useModalLayer('GlassDialogContent', 'GlassDialog');\n  const { preferences } = useGlassTheme();\n  const [present, hide] = useLayerPresence(open, preferences.reduceMotion);\n  const close = useCallback(() => setOpen(false), [setOpen]);\n\n  return (\n    <ModalLayerHost onRequestClose={close} present={present}>\n      <DialogLayer {...props} onClose={close} onHidden={hide} open={open} panelRef={ref} />\n    </ModalLayerHost>\n  );\n});\n\nGlassDialogContent.displayName = 'GlassDialogContent';\n\n/** The dialog's heading, announced as a header. */\nexport const GlassDialogTitle = forwardRef<Text, TextProps>((props, ref) => (\n  <ModalLayerTitle ref={ref} {...props} />\n));\n\nGlassDialogTitle.displayName = 'GlassDialogTitle';\n\n/** The dialog's supporting text. */\nexport const GlassDialogDescription = forwardRef<Text, TextProps>((props, ref) => (\n  <ModalLayerDescription ref={ref} {...props} />\n));\n\nGlassDialogDescription.displayName = 'GlassDialogDescription';\n\nconst styles = StyleSheet.create({\n  close: { end: 16, top: 16 },\n  frame: { maxHeight: '100%', maxWidth: 520, width: '100%' },\n  panel: { borderCurve: 'continuous', borderRadius: GLASS_RADIUS.lg, flexShrink: 1 },\n  stage: { alignItems: 'center', justifyContent: 'center' },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-dialog.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/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"
    }
  ],
  "type": "registry:ui"
}