{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-tabs",
  "title": "Glass Tabs",
  "description": "Tabs whose glass bubble springs to the selected tab.",
  "registryDependencies": [
    "@glassyui-native/glass-panel",
    "@glassyui-native/glass-provider",
    "@glassyui-native/glass-surface"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-tabs.tsx",
      "content": "import {\n  Children,\n  createContext,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  type ReactNode,\n} from 'react';\nimport {\n  Animated,\n  Pressable,\n  StyleSheet,\n  Text,\n  View,\n  type LayoutChangeEvent,\n  type LayoutRectangle,\n  type PressableProps,\n  type StyleProp,\n  type ViewProps,\n  type ViewStyle,\n} from 'react-native';\nimport { GlassPanel, type GlassPanelProps } from './glass-panel';\nimport { useGlassTheme } from './glass-provider';\nimport { GlassSurface } from './glass-surface';\nimport { GLASS_RADIUS } from './lib/tokens';\nimport { useControllableState } from './lib/use-controllable-state';\nimport { useSelectionIndicator } from './lib/use-selection-indicator';\n\ntype TabsContextValue = {\n  select: (value: string) => void;\n  selected: string;\n};\n\nconst TabsContext = createContext<TabsContextValue | null>(null);\n\nfunction useTabs(part: string) {\n  const context = useContext(TabsContext);\n\n  if (!context) throw new Error(`${part} must be used within GlassTabs`);\n\n  return context;\n}\n\ntype TabsListContextValue = {\n  forgetItem: (value: string) => void;\n  onItemLayout: (value: string, layout: LayoutRectangle) => void;\n};\n\nconst TabsListContext = createContext<TabsListContextValue | null>(null);\n\nfunction useTabsList() {\n  const context = useContext(TabsListContext);\n\n  if (!context) throw new Error('GlassTabsTrigger must be used within GlassTabsList');\n\n  return context;\n}\n\n// The raised bubble under the active trigger, from .lg-tabs-trigger[data-state='active'].\nconst BUBBLE = {\n  dark: {\n    backgroundColor: 'rgba(255, 255, 255, 0.12)',\n    boxShadow:\n      'inset 0px 1px 0px rgba(255, 255, 255, 0.22), inset 0px -1px 0px rgba(0, 0, 0, 0.12), 0px 1px 3px rgba(0, 0, 0, 0.24), 0px 4px 12px -4px rgba(0, 0, 0, 0.4)',\n  },\n  light: {\n    backgroundColor: 'rgba(255, 255, 255, 0.82)',\n    boxShadow:\n      'inset 0px 1px 0px rgba(255, 255, 255, 0.9), 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 4px 12px -4px rgba(0, 0, 0, 0.16)',\n  },\n} as const;\n\nconst INSET = 4;\n\n// The list's inset tops a 36-point trigger up to a 44-point touch.\nconst TRIGGER_HIT_SLOP = { bottom: INSET, top: INSET } as const;\n\nexport type GlassTabsProps = ViewProps & {\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  value?: string;\n};\n\n/** Tabs that show one GlassTabsContent at a time, chosen from a GlassTabsList. */\nexport const GlassTabs = forwardRef<View, GlassTabsProps>(\n  ({ defaultValue, onValueChange, value, ...rest }, ref) => {\n    const [selected, select] = useControllableState(value, defaultValue ?? '', onValueChange);\n    const context = useMemo(() => ({ select, selected }), [select, selected]);\n\n    return (\n      <TabsContext.Provider value={context}>\n        <View ref={ref} {...rest} />\n      </TabsContext.Provider>\n    );\n  },\n);\n\nGlassTabs.displayName = 'GlassTabs';\n\nexport type GlassTabsListProps = ViewProps;\n\n/**\n * The row of triggers, in a glass capsule. One raised bubble springs between them; Reduce Motion\n * moves it at once.\n */\nexport const GlassTabsList = forwardRef<View, GlassTabsListProps>(\n  ({ children, onLayout, style, ...rest }, ref) => {\n    const { preferences } = useGlassTheme();\n    const { selected } = useTabs('GlassTabsList');\n\n    const { forgetItem, onItemLayout, onTrackLayout, placement } = useSelectionIndicator(\n      selected,\n      preferences.reduceMotion,\n    );\n\n    const handleLayout = useCallback(\n      (event: LayoutChangeEvent) => {\n        onTrackLayout(event);\n        onLayout?.(event);\n      },\n      [onLayout, onTrackLayout],\n    );\n\n    const list = useMemo(() => ({ forgetItem, onItemLayout }), [forgetItem, onItemLayout]);\n\n    return (\n      <TabsListContext.Provider value={list}>\n        <GlassSurface\n          ref={ref}\n          accessibilityRole=\"tablist\"\n          elevation=\"base\"\n          onLayout={handleLayout}\n          preset=\"control\"\n          style={[styles.list, style]}\n          {...rest}\n        >\n          {placement ? (\n            <Animated.View\n              style={[\n                { pointerEvents: 'none' },\n                styles.indicator,\n                BUBBLE[preferences.theme],\n                {\n                  height: placement.height,\n                  top: placement.top,\n                  width: placement.width,\n                  transform: [{ translateX: placement.offset }],\n                },\n              ]}\n            />\n          ) : null}\n          {children}\n        </GlassSurface>\n      </TabsListContext.Provider>\n    );\n  },\n);\n\nGlassTabsList.displayName = 'GlassTabsList';\n\nexport type GlassTabsTriggerProps = Omit<PressableProps, 'children' | 'onPress' | 'style'> & {\n  children?: ReactNode;\n  style?: StyleProp<ViewStyle>;\n  value: string;\n};\n\n/** Shows the GlassTabsContent with the same value. Plain text children are set as its label. */\nexport const GlassTabsTrigger = forwardRef<View, GlassTabsTriggerProps>(\n  ({ children, disabled, onLayout, style, value, ...rest }, ref) => {\n    const { colors } = useGlassTheme();\n    const { select, selected } = useTabs('GlassTabsTrigger');\n    const { forgetItem, onItemLayout } = useTabsList();\n    const layout = useRef<LayoutRectangle | null>(null);\n    const active = selected === value;\n\n    // Re-registers under a new value, and leaves the bubble nothing to find once unmounted.\n    useEffect(() => {\n      if (layout.current) onItemLayout(value, layout.current);\n\n      return () => forgetItem(value);\n    }, [forgetItem, onItemLayout, value]);\n\n    const handleLayout = useCallback(\n      (event: LayoutChangeEvent) => {\n        layout.current = event.nativeEvent.layout;\n        onItemLayout(value, event.nativeEvent.layout);\n        onLayout?.(event);\n      },\n      [onItemLayout, onLayout, value],\n    );\n\n    const show = useCallback(() => {\n      if (!active) select(value);\n    }, [active, select, value]);\n\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"tab\"\n        accessibilityState={{ disabled: Boolean(disabled), selected: active }}\n        disabled={disabled}\n        hitSlop={TRIGGER_HIT_SLOP}\n        onLayout={handleLayout}\n        onPress={show}\n        style={[styles.trigger, disabled && styles.disabled, style]}\n        {...rest}\n      >\n        {Children.toArray(children).every((child) => !isValidElement(child)) ? (\n          <Text\n            numberOfLines={1}\n            style={[styles.label, { color: active ? colors.foreground : colors.mutedForeground }]}\n          >\n            {children}\n          </Text>\n        ) : (\n          children\n        )}\n      </Pressable>\n    );\n  },\n);\n\nGlassTabsTrigger.displayName = 'GlassTabsTrigger';\n\nexport type GlassTabsContentProps = Omit<GlassPanelProps, 'variant'> & {\n  value: string;\n};\n\n/** The panel for one trigger, on a glass card. It renders only while its trigger is active. */\nexport const GlassTabsContent = forwardRef<View, GlassTabsContentProps>(\n  ({ children, style, value, ...rest }, ref) => {\n    const { colors } = useGlassTheme();\n    const { selected } = useTabs('GlassTabsContent');\n\n    if (selected !== value) return null;\n\n    return (\n      <GlassPanel\n        ref={ref}\n        elevation=\"base\"\n        preset=\"subtle\"\n        variant=\"card\"\n        style={[styles.content, style]}\n        {...rest}\n      >\n        {Children.toArray(children).every((child) => !isValidElement(child)) ? (\n          <Text style={[styles.body, { color: colors.foreground }]}>{children}</Text>\n        ) : (\n          children\n        )}\n      </GlassPanel>\n    );\n  },\n);\n\nGlassTabsContent.displayName = 'GlassTabsContent';\n\nconst styles = StyleSheet.create({\n  body: { fontSize: 14, lineHeight: 20 },\n  content: { marginTop: 12 },\n  disabled: { opacity: 0.45 },\n  indicator: {\n    borderCurve: 'continuous',\n    borderRadius: GLASS_RADIUS.capsule,\n    position: 'absolute',\n    start: 0,\n  },\n  label: { fontSize: 14, lineHeight: 20 },\n  list: {\n    alignSelf: 'stretch',\n    borderRadius: GLASS_RADIUS.capsule,\n    flexDirection: 'row',\n    gap: 4,\n    padding: INSET,\n  },\n  trigger: {\n    alignItems: 'center',\n    borderCurve: 'continuous',\n    borderRadius: GLASS_RADIUS.capsule,\n    flex: 1,\n    justifyContent: 'center',\n    minWidth: 0,\n    paddingHorizontal: 12,\n    paddingVertical: 8,\n  },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-tabs.tsx"
    },
    {
      "path": "registry/glassyui-native/lib/use-selection-indicator.ts",
      "content": "import { useCallback, useLayoutEffect, useRef, useState } from 'react';\nimport { Animated, I18nManager, type LayoutChangeEvent, type LayoutRectangle } from 'react-native';\nimport { GLASS_MOTION } from './tokens';\n\n/** Where the indicator sits: the selected item's box, and the offset that slides it there. */\nexport type IndicatorPlacement = {\n  height: number;\n  offset: Animated.Value;\n  top: number;\n  width: number;\n};\n\nfunction sameBox(previous: LayoutRectangle | undefined, next: LayoutRectangle) {\n  return (\n    previous !== undefined &&\n    previous.x === next.x &&\n    previous.y === next.y &&\n    previous.width === next.width &&\n    previous.height === next.height\n  );\n}\n\n/**\n * Slides one indicator between the items of a track, such as a segmented control's options or\n * a tab list's triggers. Items report their layout; the indicator sits at the track's start edge\n * and springs to the selected item. It jumps instead on its first placement, after a layout\n * change and under Reduce Motion.\n */\nexport function useSelectionIndicator(selected: string, reduceMotion: boolean) {\n  const [items, setItems] = useState<ReadonlyMap<string, LayoutRectangle>>(() => new Map());\n  const [trackWidth, setTrackWidth] = useState(0);\n  const [offset] = useState(() => new Animated.Value(0));\n  const placedOn = useRef<string | null>(null);\n  const box = items.get(selected);\n  const measured = box !== undefined;\n\n  // Layout reports physical positions. In RTL the start edge is the track's right edge, so the\n  // indicator travels left from there.\n  const start = measured ? (I18nManager.isRTL ? box.x + box.width - trackWidth : box.x) : 0;\n\n  useLayoutEffect(() => {\n    if (!measured) {\n      placedOn.current = null;\n\n      return;\n    }\n\n    // A new selection slides; the first placement and a relayout of the same item jump.\n    if (reduceMotion || placedOn.current === null || placedOn.current === selected) {\n      offset.setValue(start);\n    } else {\n      Animated.spring(offset, {\n        ...GLASS_MOTION.spring,\n        toValue: start,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }).start();\n    }\n\n    placedOn.current = selected;\n  }, [measured, offset, reduceMotion, selected, start]);\n\n  const onTrackLayout = useCallback((event: LayoutChangeEvent) => {\n    setTrackWidth(event.nativeEvent.layout.width);\n  }, []);\n\n  const onItemLayout = useCallback((value: string, layout: LayoutRectangle) => {\n    setItems((previous) =>\n      sameBox(previous.get(value), layout) ? previous : new Map(previous).set(value, layout),\n    );\n  }, []);\n\n  const forgetItem = useCallback((value: string) => {\n    setItems((previous) => {\n      if (!previous.has(value)) return previous;\n\n      const next = new Map(previous);\n\n      next.delete(value);\n\n      return next;\n    });\n  }, []);\n\n  const placement: IndicatorPlacement | null = measured\n    ? { height: box.height, offset, top: box.y, width: box.width }\n    : null;\n\n  return { forgetItem, onItemLayout, onTrackLayout, placement };\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/use-selection-indicator.ts"
    }
  ],
  "type": "registry:ui"
}