{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-dock",
  "title": "Glass Dock",
  "description": "A row of icons on a glass shelf for an app's main destinations.",
  "registryDependencies": [
    "@glassyui-native/glass-provider",
    "@glassyui-native/glass-surface"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-dock.tsx",
      "content": "import { forwardRef, useCallback, useEffect, useState, type ReactNode } from 'react';\nimport {\n  Animated,\n  Easing,\n  Pressable,\n  StyleSheet,\n  Text,\n  View,\n  type GestureResponderEvent,\n  type MouseEvent,\n  type NativeSyntheticEvent,\n  type PressableProps,\n  type StyleProp,\n  type TargetedEvent,\n  type ViewProps,\n  type ViewStyle,\n} from 'react-native';\nimport { useGlassTheme } from './glass-provider';\nimport { GlassSurface } from './glass-surface';\nimport { wrapPlainText } from './lib/plain-text';\nimport { GLASS_MOTION } from './lib/tokens';\n\nexport type GlassDockProps = ViewProps;\n\nexport type GlassDockItemProps = Omit<PressableProps, 'children' | 'style'> & {\n  /** Shows the running indicator under the icon. */\n  active?: boolean;\n  /** The icon. Full app artwork can fill the item; a small glyph sits in its middle. */\n  children?: ReactNode;\n  /** Names the item for assistive technology, and shows above it while it is held. */\n  label: string;\n  style?: StyleProp<ViewStyle>;\n};\n\nexport type GlassDockSeparatorProps = ViewProps;\n\nconst ITEM = 48;\n\nconst GAP = 8;\n\nconst PAD = 10;\n\n// The web's ease-out curve for arrivals; exits ease in and run shorter.\nconst EASE_OUT = Easing.bezier(0.32, 0.72, 0, 1);\n\nconst EASE_IN = Easing.bezier(0.42, 0, 1, 1);\n\ntype DockLabelProps = {\n  /** True once nothing holds the label up; it shrinks back into its item, then `onLeft` runs. */\n  leaving: boolean;\n  onLeft: () => void;\n  text: string;\n};\n\n// The label is glass, so it grows out of its item instead of fading in.\nfunction DockLabel({ leaving, onLeft, text }: DockLabelProps) {\n  const { colors, preferences } = useGlassTheme();\n  const [grow] = useState(() => new Animated.Value(preferences.reduceMotion ? 1 : 0));\n\n  useEffect(() => {\n    if (preferences.reduceMotion) {\n      grow.setValue(leaving ? 0 : 1);\n\n      if (leaving) onLeft();\n\n      return;\n    }\n\n    const animation = Animated.timing(grow, {\n      duration: leaving ? 150 : 220,\n      easing: leaving ? EASE_IN : EASE_OUT,\n      toValue: leaving ? 0 : 1,\n      useNativeDriver: GLASS_MOTION.nativeDriver,\n    });\n\n    animation.start(({ finished }) => {\n      if (finished && leaving) onLeft();\n    });\n\n    return () => animation.stop();\n  }, [grow, leaving, onLeft, preferences.reduceMotion]);\n\n  return (\n    <View style={styles.labelRow}>\n      <Animated.View\n        style={[\n          styles.labelOrigin,\n          {\n            transform: [\n              { translateY: grow.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) },\n              { scale: grow.interpolate({ inputRange: [0, 1], outputRange: [0.2, 1] }) },\n            ],\n          },\n        ]}\n      >\n        <GlassSurface preset=\"overlay\" style={styles.label}>\n          <Animated.Text\n            numberOfLines={1}\n            style={[\n              styles.labelText,\n              {\n                color: colors.foreground,\n                opacity: grow.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0, 0, 1] }),\n              },\n            ]}\n          >\n            {text}\n          </Animated.Text>\n        </GlassSurface>\n      </Animated.View>\n    </View>\n  );\n}\n\n/**\n * A row of app icons on a quiet glass shelf. Holding an icon shows its label; nothing a finger\n * does resizes an icon or its neighbours.\n */\nexport const GlassDock = forwardRef<View, GlassDockProps>(({ children, style, ...rest }, ref) => (\n  <View ref={ref} style={[styles.dock, style]} {...rest}>\n    <GlassSurface\n      accessibilityElementsHidden\n      importantForAccessibility=\"no-hide-descendants\"\n      preset=\"standard\"\n      style={[StyleSheet.absoluteFill, styles.shelf]}\n    />\n    {children}\n  </View>\n));\n\nGlassDock.displayName = 'GlassDock';\n\n/**\n * An icon in the dock. A long press shows its label above it, as a pointer's hover and keyboard\n * focus do; a dot beneath marks it active.\n */\nexport const GlassDockItem = forwardRef<View, GlassDockItemProps>(\n  (\n    {\n      active = false,\n      children,\n      disabled,\n      label,\n      onBlur,\n      onFocus,\n      onHoverIn,\n      onHoverOut,\n      onLongPress,\n      onPressOut,\n      style,\n      ...rest\n    },\n    ref,\n  ) => {\n    const { colors, preferences } = useGlassTheme();\n    const [held, setHeld] = useState(false);\n    const [hovered, setHovered] = useState(false);\n    const [focused, setFocused] = useState(false);\n    const [labelShown, setLabelShown] = useState(false);\n    const [dot] = useState(() => new Animated.Value(active ? 1 : 0));\n\n    useEffect(() => {\n      if (preferences.reduceMotion) {\n        dot.setValue(active ? 1 : 0);\n\n        return;\n      }\n\n      const animation = Animated.timing(dot, {\n        duration: 200,\n        toValue: active ? 1 : 0,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      });\n\n      animation.start();\n\n      return () => animation.stop();\n    }, [active, dot, preferences.reduceMotion]);\n\n    const handleLongPress = useCallback(\n      (event: GestureResponderEvent) => {\n        onLongPress?.(event);\n        setHeld(true);\n        setLabelShown(true);\n      },\n      [onLongPress],\n    );\n\n    const handlePressOut = useCallback(\n      (event: GestureResponderEvent) => {\n        onPressOut?.(event);\n        setHeld(false);\n      },\n      [onPressOut],\n    );\n\n    const handleHoverIn = useCallback(\n      (event: MouseEvent) => {\n        onHoverIn?.(event);\n        setHovered(true);\n        setLabelShown(true);\n      },\n      [onHoverIn],\n    );\n\n    const handleHoverOut = useCallback(\n      (event: MouseEvent) => {\n        onHoverOut?.(event);\n        setHovered(false);\n      },\n      [onHoverOut],\n    );\n\n    const handleFocus = useCallback(\n      (event: NativeSyntheticEvent<TargetedEvent>) => {\n        onFocus?.(event);\n        setFocused(true);\n        setLabelShown(true);\n      },\n      [onFocus],\n    );\n\n    const handleBlur = useCallback(\n      (event: NativeSyntheticEvent<TargetedEvent>) => {\n        onBlur?.(event);\n        setFocused(false);\n      },\n      [onBlur],\n    );\n\n    const hideLabel = useCallback(() => setLabelShown(false), []);\n\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityLabel={label}\n        accessibilityRole=\"button\"\n        accessibilityState={{ disabled: Boolean(disabled) }}\n        disabled={disabled}\n        onBlur={handleBlur}\n        onFocus={handleFocus}\n        onHoverIn={handleHoverIn}\n        onHoverOut={handleHoverOut}\n        onLongPress={handleLongPress}\n        onPressOut={handlePressOut}\n        style={({ pressed }) => [\n          styles.item,\n          (pressed || hovered || focused) && { backgroundColor: colors.controlSurface },\n          focused && { outlineColor: colors.focusRing },\n          focused && styles.focused,\n          disabled && styles.disabled,\n          style,\n        ]}\n        {...rest}\n      >\n        {wrapPlainText(children, (text) => (\n          <Text style={[styles.glyph, { color: colors.foreground }]}>{text}</Text>\n        ))}\n        {labelShown ? (\n          <DockLabel leaving={!(held || hovered || focused)} onLeft={hideLabel} text={label} />\n        ) : null}\n        <View style={styles.dotRow}>\n          <Animated.View\n            style={[\n              styles.dot,\n              {\n                backgroundColor: colors.foreground,\n                opacity: dot.interpolate({ inputRange: [0, 1], outputRange: [0, 0.75] }),\n              },\n            ]}\n          />\n        </View>\n      </Pressable>\n    );\n  },\n);\n\nGlassDockItem.displayName = 'GlassDockItem';\n\n/** A thin upright line between groups of dock icons. */\nexport const GlassDockSeparator = forwardRef<View, GlassDockSeparatorProps>(\n  ({ 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);\n\nGlassDockSeparator.displayName = 'GlassDockSeparator';\n\nconst styles = StyleSheet.create({\n  disabled: { opacity: 0.45 },\n  // The shelf hugs its icons, like the web's inline dock; pass alignSelf to place it.\n  dock: {\n    alignItems: 'center',\n    alignSelf: 'flex-start',\n    flexDirection: 'row',\n    gap: GAP,\n    height: ITEM + PAD * 2,\n    paddingHorizontal: PAD + 2,\n    paddingVertical: PAD,\n  },\n  dot: { borderRadius: 2, height: 4, width: 4 },\n  // The indicator sits in the shelf's padding, under the icon.\n  dotRow: {\n    alignItems: 'center',\n    bottom: -(PAD - 3),\n    left: 0,\n    pointerEvents: 'none',\n    position: 'absolute',\n    right: 0,\n  },\n  focused: { outlineOffset: 3, outlineStyle: 'solid', outlineWidth: 2 },\n  glyph: { fontSize: 28 },\n  // The same generous target holds full artwork or a small glyph, and never changes size.\n  item: {\n    alignItems: 'center',\n    borderCurve: 'continuous',\n    borderRadius: ITEM * 0.225,\n    height: ITEM,\n    justifyContent: 'center',\n    width: ITEM,\n  },\n  label: { borderRadius: 10, paddingHorizontal: 10, paddingVertical: 6 },\n  labelOrigin: { transformOrigin: 'bottom' },\n  // Wide enough to centre any label over the icon; it takes no touches.\n  labelRow: {\n    alignItems: 'center',\n    bottom: '100%',\n    left: -200,\n    marginBottom: 10,\n    pointerEvents: 'none',\n    position: 'absolute',\n    right: -200,\n  },\n  labelText: { fontSize: 13, fontWeight: '500', lineHeight: 18 },\n  separator: { alignSelf: 'stretch', marginHorizontal: 4, marginVertical: 6, width: 1 },\n  shelf: { borderRadius: ITEM * 0.24 + PAD, pointerEvents: 'none' },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-dock.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"
}