{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-alert",
  "title": "Glass Alert",
  "description": "A short message set in tinted glass, with four tones and an optional dismiss.",
  "dependencies": [
    "react-native-svg"
  ],
  "registryDependencies": [
    "@glassyui-native/glass-provider",
    "@glassyui-native/glass-surface"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-alert.tsx",
      "content": "import { forwardRef, useCallback, useEffect, useRef, useState, type ReactNode } from 'react';\nimport {\n  Animated,\n  Easing,\n  Pressable,\n  StyleSheet,\n  Text,\n  View,\n  type LayoutChangeEvent,\n  type ViewProps,\n} from 'react-native';\nimport { useGlassTheme } from './glass-provider';\nimport { GlassSurface } from './glass-surface';\nimport { GlassIcon } from './lib/icons';\nimport { wrapPlainText } from './lib/plain-text';\nimport { GLASS_MOTION, GLASS_RADIUS } from './lib/tokens';\n\nexport type GlassAlertTone = 'info' | 'success' | 'warning' | 'danger';\n\nexport type GlassAlertProps = ViewProps & {\n  /** Names the close button. Defaults to \"Dismiss\". */\n  dismissLabel?: string;\n  /** Replaces the tone's icon. Pass `false` for none. */\n  icon?: ReactNode | false;\n  /**\n   * Shows a close button. The alert folds away, then this runs: remove the alert here, and give\n   * each alert a stable `key` so a later one never inherits the fold.\n   */\n  onDismiss?: () => void;\n  title?: ReactNode;\n  tone?: GlassAlertTone;\n};\n\n// The alert's own palette: the badge's tones, tinting the glass. Each keeps 4.5:1 text over a\n// white or black backdrop in both themes.\nconst TONES = {\n  dark: {\n    info: { fill: 'rgb(13, 44, 82)', ink: '#b2d6ff', tint: '0, 103, 217' },\n    success: { fill: 'rgb(22, 60, 48)', ink: '#7cebc0', tint: '51, 214, 159' },\n    warning: { fill: 'rgb(70, 54, 22)', ink: '#ffd27a', tint: '255, 189, 56' },\n    danger: { fill: 'rgb(76, 30, 44)', ink: '#ffa6b7', tint: '255, 92, 122' },\n  },\n  light: {\n    info: { fill: 'rgb(229, 241, 255)', ink: '#0054b3', tint: '0, 103, 217' },\n    success: { fill: 'rgb(224, 247, 238)', ink: '#08603f', tint: '20, 168, 116' },\n    warning: { fill: 'rgb(255, 243, 219)', ink: '#824c00', tint: '232, 150, 12' },\n    danger: { fill: 'rgb(255, 229, 236)', ink: '#a8113a', tint: '232, 52, 90' },\n  },\n} as const;\n\nconst HIGHLIGHT = { dark: 'rgba(255, 255, 255, 0.12)', light: 'rgba(255, 255, 255, 0.9)' } as const;\n\n// The close button lights under a finger, as it does under the web's pointer.\nconst CLOSE_PRESSED = { dark: 'rgba(255, 255, 255, 0.14)', light: 'rgba(0, 0, 0, 0.07)' } as const;\n\nconst RADIUS = 18;\n\n// The row folds on the web's ease-out curve while the content fades out faster, easing in.\nconst EASE_OUT = Easing.bezier(0.32, 0.72, 0, 1);\n\nconst EASE_IN = Easing.bezier(0.42, 0, 1, 1);\n\nconst FOLD_MS = 240;\n\nconst FADE_MS = 160;\n\n/**\n * A short message set in tinted glass. On Android, warnings and errors interrupt the screen reader;\n * the other tones wait their turn.\n */\nexport const GlassAlert = forwardRef<View, GlassAlertProps>(\n  (\n    {\n      children,\n      dismissLabel = 'Dismiss',\n      icon,\n      onDismiss,\n      style,\n      testID,\n      title,\n      tone = 'info',\n      ...rest\n    },\n    ref,\n  ) => {\n    const { colors, preferences } = useGlassTheme();\n    const palette = TONES[preferences.theme][tone];\n    const radius = StyleSheet.flatten(style)?.borderRadius ?? RADIUS;\n    const [height, setHeight] = useState(0);\n    const [folding, setFolding] = useState(false);\n    const [fold] = useState(() => new Animated.Value(1));\n    const [fade] = useState(() => new Animated.Value(1));\n\n    // The fold ends in the handler the caller has now, not the one they had at the press, and it\n    // ends once.\n    const dismissed = useRef(onDismiss);\n    const finished = useRef(false);\n\n    useEffect(() => {\n      dismissed.current = onDismiss;\n    }, [onDismiss]);\n\n    // Unmounting stops the fold, so a caller who removed the alert first is not told about it.\n    useEffect(\n      () => () => {\n        fade.stopAnimation();\n        fold.stopAnimation();\n      },\n      [fade, fold],\n    );\n\n    const measure = useCallback(\n      (event: LayoutChangeEvent) => {\n        if (!folding) setHeight(event.nativeEvent.layout.height);\n      },\n      [folding],\n    );\n\n    const dismiss = useCallback(() => {\n      if (folding) return;\n\n      if (preferences.reduceMotion) {\n        onDismiss?.();\n\n        return;\n      }\n\n      setFolding(true);\n      // The glass keeps its backdrop, so only the content fades; the glass folds with the row.\n      Animated.timing(fade, {\n        duration: FADE_MS,\n        easing: EASE_IN,\n        toValue: 0,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }).start();\n      // The row shrinks to nothing, so what is below the alert slides up instead of jumping.\n      Animated.timing(fold, {\n        duration: FOLD_MS,\n        easing: EASE_OUT,\n        toValue: 0,\n        // Height is layout, which only the JavaScript driver can animate.\n        useNativeDriver: false,\n      }).start(({ finished: done }) => {\n        if (!done || finished.current) return;\n\n        finished.current = true;\n        dismissed.current?.();\n      });\n    }, [fade, fold, folding, onDismiss, preferences.reduceMotion]);\n\n    return (\n      <Animated.View\n        onLayout={measure}\n        style={\n          folding\n            ? {\n                height: fold.interpolate({ inputRange: [0, 1], outputRange: [0, height] }),\n                overflow: 'hidden',\n              }\n            : null\n        }\n      >\n        <GlassSurface\n          ref={ref}\n          accessibilityLiveRegion={tone === 'warning' || tone === 'danger' ? 'assertive' : 'polite'}\n          accessibilityRole=\"alert\"\n          elevation=\"none\"\n          preset=\"standard\"\n          style={[styles.alert, style]}\n          testID={testID}\n          tint={palette.fill}\n          {...rest}\n        >\n          <View\n            style={[\n              StyleSheet.absoluteFill,\n              styles.ring,\n              {\n                borderRadius: radius,\n                boxShadow: `inset 0px 1px 0px ${HIGHLIGHT[preferences.theme]}, inset 0px 0px 0px 0.5px rgba(${palette.tint}, 0.45)`,\n              },\n            ]}\n          />\n          <Animated.View style={[styles.row, { opacity: fade }]}>\n            {icon === false ? null : (\n              <View\n                accessibilityElementsHidden\n                importantForAccessibility=\"no-hide-descendants\"\n                style={styles.icon}\n              >\n                {icon ?? <GlassIcon color={palette.ink} name={tone} strokeWidth={1.6} />}\n              </View>\n            )}\n            <View style={styles.body}>\n              {title\n                ? wrapPlainText(title, (text) => (\n                    <Text style={[styles.title, { color: colors.foreground }]}>{text}</Text>\n                  ))\n                : null}\n              {children\n                ? wrapPlainText(children, (text) => (\n                    <Text style={[styles.text, { color: colors.mutedForeground }]}>{text}</Text>\n                  ))\n                : null}\n            </View>\n            {onDismiss ? (\n              <Pressable\n                accessibilityLabel={dismissLabel}\n                accessibilityRole=\"button\"\n                hitSlop={8}\n                onPress={dismiss}\n                style={({ pressed }) => [\n                  styles.close,\n                  pressed && { backgroundColor: CLOSE_PRESSED[preferences.theme], opacity: 1 },\n                ]}\n                testID={testID ? `${testID}-dismiss` : undefined}\n              >\n                <GlassIcon color={colors.foreground} name=\"close\" size={16} strokeWidth={1.6} />\n              </Pressable>\n            ) : null}\n          </Animated.View>\n        </GlassSurface>\n      </Animated.View>\n    );\n  },\n);\n\nGlassAlert.displayName = 'GlassAlert';\n\nconst styles = StyleSheet.create({\n  alert: {\n    borderCurve: 'continuous',\n    borderRadius: RADIUS,\n    paddingHorizontal: 16,\n    paddingVertical: 14,\n    width: '100%',\n  },\n  body: { flex: 1, gap: 2, minWidth: 0 },\n  // A 28-point circle that still answers a 44-point touch, set into the first line's height.\n  close: {\n    alignItems: 'center',\n    borderRadius: GLASS_RADIUS.capsule,\n    height: 28,\n    justifyContent: 'center',\n    marginEnd: -6,\n    marginVertical: -4,\n    opacity: 0.7,\n    width: 28,\n  },\n  icon: { height: 20, marginTop: 1, width: 20 },\n  ring: { borderCurve: 'continuous', pointerEvents: 'none' },\n  row: { alignItems: 'flex-start', flexDirection: 'row', gap: 12 },\n  text: { fontSize: 15, lineHeight: 21 },\n  title: { fontSize: 15, fontWeight: '600', lineHeight: 21 },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-alert.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"
    }
  ],
  "type": "registry:ui"
}