{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-card-stack",
  "title": "Glass Card Stack",
  "description": "A stack of cards that follows your finger and settles with a velocity-aware spring, with navigation controls and selection haptics.",
  "dependencies": [
    "react-native-gesture-handler",
    "react-native-reanimated",
    "react-native-worklets",
    "expo-haptics"
  ],
  "registryDependencies": [
    "@glassyui-native/glass-surface",
    "@glassyui-native/glass-provider",
    "@glassyui-native/glass-button"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-card-stack.tsx",
      "content": "import { useEffect, type ReactNode } from 'react';\nimport { StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { Gesture, GestureDetector } from 'react-native-gesture-handler';\nimport Animated, {\n  ReduceMotion,\n  useAnimatedStyle,\n  useSharedValue,\n  withSpring,\n  type SharedValue,\n} from 'react-native-reanimated';\nimport { scheduleOnRN } from 'react-native-worklets';\nimport { GlassButton } from './glass-button';\nimport { useGlassTheme } from './glass-provider';\nimport { GlassSurface } from './glass-surface';\nimport { selectionHaptic } from './lib/selection-haptic';\nimport { GLASS_MOTION } from './lib/tokens';\n\nexport type GlassCardStackProps<T extends { id: string }> = {\n  items: readonly T[];\n  activeIndex: number;\n  onIndexChange: (index: number) => void;\n  renderCard: (item: T, index: number) => ReactNode;\n  label: string;\n  height?: number;\n  haptics?: boolean;\n  style?: StyleProp<ViewStyle>;\n  testID?: string;\n};\n\nfunction StackCard({\n  children,\n  cardIndex,\n  count,\n  position,\n  width,\n  active,\n}: {\n  children: ReactNode;\n  cardIndex: number;\n  count: number;\n  position: SharedValue<number>;\n  width: SharedValue<number>;\n  active: boolean;\n}) {\n  const { preferences } = useGlassTheme();\n\n  const animated = useAnimatedStyle(() => {\n    const distance = cardIndex - position.get();\n    const depth = Math.min(Math.max(distance, 0), 3);\n\n    return {\n      transform: [\n        { translateX: distance < 0 ? distance * (width.get() + 32) : 0 },\n        { translateY: depth * 12 },\n        { scale: preferences.reduceMotion ? 1 : 1 - depth * 0.04 },\n        { rotate: `${preferences.reduceMotion ? 0 : Math.min(distance, 0) * 7}deg` },\n      ],\n    };\n  });\n\n  return (\n    <Animated.View\n      accessibilityElementsHidden={!active}\n      importantForAccessibility={active ? 'auto' : 'no-hide-descendants'}\n      pointerEvents={active ? 'auto' : 'none'}\n      style={[StyleSheet.absoluteFill, { zIndex: count - cardIndex }, animated]}\n    >\n      <GlassSurface style={styles.card}>{children}</GlassSurface>\n    </Animated.View>\n  );\n}\n\n/** A controlled card deck: direct drag, velocity-aware spring release, and ordinary navigation. */\nexport function GlassCardStack<T extends { id: string }>({\n  items,\n  activeIndex,\n  onIndexChange,\n  renderCard,\n  label,\n  height = 220,\n  haptics = true,\n  style,\n  testID,\n}: GlassCardStackProps<T>) {\n  const { colors, preferences } = useGlassTheme();\n  const index = Math.max(0, Math.min(activeIndex, items.length - 1));\n  const position = useSharedValue(index);\n  const target = useSharedValue(index);\n  const origin = useSharedValue(index);\n  const width = useSharedValue(1);\n  const reduceMotion = preferences.reduceMotion;\n\n  const spring = {\n    ...GLASS_MOTION.spring,\n    reduceMotion: reduceMotion ? ReduceMotion.Always : ReduceMotion.System,\n  };\n\n  useEffect(() => {\n    if (reduceMotion) position.set(index);\n    else if (target.get() !== index) position.set(withSpring(index, GLASS_MOTION.spring));\n    target.set(index);\n  }, [index, position, reduceMotion, target]);\n\n  function select(next: number) {\n    if (next === index) return;\n    selectionHaptic(haptics);\n    onIndexChange(next);\n  }\n\n  function navigate(next: number) {\n    target.set(next);\n    position.set(reduceMotion ? next : withSpring(next, spring));\n    select(next);\n  }\n\n  const pan = Gesture.Pan()\n    .enabled(items.length > 1)\n    .activeOffsetX([-10, 10])\n    .failOffsetY([-10, 10])\n    .onBegin(() => {\n      origin.set(position.get());\n    })\n    .onUpdate((event) => {\n      if (reduceMotion) return;\n      const next = origin.get() - event.translationX / width.get();\n      // Edge travel resists the finger instead of implying another card exists.\n      position.set(\n        next < 0\n          ? next * 0.18\n          : next > items.length - 1\n            ? items.length - 1 + (next - items.length + 1) * 0.18\n            : next,\n      );\n    })\n    .onEnd((event) => {\n      const displacement = -event.translationX / width.get();\n      const velocity = -event.velocityX / width.get();\n\n      const direction =\n        Math.abs(displacement) > 0.22 || Math.abs(event.velocityX) > 600\n          ? displacement + velocity * 0.1 > 0\n            ? 1\n            : -1\n          : 0;\n\n      const next = Math.max(0, Math.min(index + direction, items.length - 1));\n      target.set(next);\n      position.set(reduceMotion ? next : withSpring(next, { ...spring, velocity }));\n\n      if (next !== index) scheduleOnRN(select, next);\n    })\n    .onFinalize((_event, success) => {\n      if (!success) position.set(reduceMotion ? index : withSpring(index, spring));\n    });\n\n  const first = Math.max(0, index - 1);\n\n  return (\n    <View style={[styles.stack, style]}>\n      <GestureDetector gesture={pan} touchAction=\"pan-y\">\n        <View\n          testID={testID}\n          onLayout={(event) => {\n            width.set(Math.max(1, event.nativeEvent.layout.width));\n          }}\n          style={[styles.deck, { height: height + 24 }]}\n        >\n          <View style={{ height }}>\n            {items.slice(first, index + 3).map((item, offset) => (\n              <StackCard\n                key={item.id}\n                cardIndex={first + offset}\n                count={items.length}\n                position={position}\n                width={width}\n                active={first + offset === index}\n              >\n                {renderCard(item, first + offset)}\n              </StackCard>\n            ))}\n          </View>\n        </View>\n      </GestureDetector>\n      <View style={styles.navigation}>\n        <GlassButton\n          accessibilityLabel={`Previous card in ${label}`}\n          disabled={index === 0 || items.length === 0}\n          onPress={() => navigate(index - 1)}\n          size=\"sm\"\n        >\n          ‹\n        </GlassButton>\n        <Text accessibilityLiveRegion=\"polite\" style={{ color: colors.mutedForeground }}>\n          {items.length ? index + 1 : 0} / {items.length}\n        </Text>\n        <GlassButton\n          accessibilityLabel={`Next card in ${label}`}\n          disabled={index >= items.length - 1}\n          onPress={() => navigate(index + 1)}\n          size=\"sm\"\n        >\n          ›\n        </GlassButton>\n      </View>\n    </View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  card: { flex: 1, padding: 20 },\n  deck: { overflow: 'hidden' },\n  navigation: { alignItems: 'center', flexDirection: 'row', gap: 16, justifyContent: 'center' },\n  stack: { gap: 12 },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-card-stack.tsx"
    },
    {
      "path": "registry/glassyui-native/lib/selection-haptic.ts",
      "content": "import { selectionAsync } from 'expo-haptics';\nimport { Platform } from 'react-native';\n\n/** Feedback follows a committed selection. Web input does not vibrate. */\nexport function selectionHaptic(enabled: boolean) {\n  if (!enabled || Platform.OS === 'web') return;\n\n  void selectionAsync().catch((error: Error) => {\n    console.warn('Glassy UI could not play selection feedback.', error.message);\n  });\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/selection-haptic.ts"
    }
  ],
  "type": "registry:ui"
}