{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "parallax-header",
  "type": "registry:item",
  "title": "Parallax Header",
  "description": "A scroll view whose header artwork scrolls at half speed, stretches on pull and hands off to a compact bar.",
  "categories": [
    "navigation"
  ],
  "dependencies": [
    "react-native-reanimated",
    "react-native-safe-area-context",
    "react-native-worklets"
  ],
  "registryDependencies": [
    "https://kinetik-ui.dev/r/kinetik-tokens.json",
    "https://kinetik-ui.dev/r/kinetik-motion.json"
  ],
  "files": [
    {
      "path": "src/components/kinetik/parallax-header.tsx",
      "type": "registry:file",
      "target": "~/components/kinetik/parallax-header.tsx",
      "content": "import type { ReactNode } from 'react';\nimport { StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport Animated, {\n  Extrapolation,\n  interpolate,\n  useAnimatedRef,\n  useAnimatedStyle,\n  useScrollOffset,\n} from 'react-native-reanimated';\nimport { useSafeAreaInsets } from 'react-native-safe-area-context';\n\nimport { useReduceMotion } from '../../lib/kinetik/hooks/use-reduce-motion';\nimport { useKinetikTheme } from '../../lib/kinetik/tokens/theme';\n\nexport type ParallaxHeaderProps = {\n  /** Scrollable content below the header. */\n  children: ReactNode;\n  /** Header artwork (image, gradient, illustration). Fills the header area. */\n  header: ReactNode;\n  /** Large title shown over the artwork and in the compact bar once scrolled. */\n  title: string;\n  /** Small line under the large title. */\n  subtitle?: string;\n  /** Height of the expanded header, in pt. Defaults to 300. */\n  headerHeight?: number;\n  /** Height of the compact bar below the status bar, in pt. Defaults to 52. */\n  barHeight?: number;\n  /** Distance from the top of the screen to the bar. Defaults to the safe-area inset. */\n  topInset?: number;\n  /** Content shown at the right of the compact bar (e.g. a button). */\n  barAccessory?: ReactNode;\n  style?: StyleProp<ViewStyle>;\n  contentContainerStyle?: StyleProp<ViewStyle>;\n};\n\n/**\n * A scroll view with a layered header: the artwork scrolls at half speed and\n * stretches when pulled down, the large title fades as it leaves, and a\n * compact bar with the title takes over at the top.\n */\nexport function ParallaxHeader({\n  children,\n  header,\n  title,\n  subtitle,\n  headerHeight = 300,\n  barHeight = 52,\n  topInset,\n  barAccessory,\n  style,\n  contentContainerStyle,\n}: ParallaxHeaderProps) {\n  const theme = useKinetikTheme();\n  const reduced = useReduceMotion();\n  const insets = useSafeAreaInsets();\n  const top = topInset ?? insets.top;\n  const ref = useAnimatedRef<Animated.ScrollView>();\n  const offset = useScrollOffset(ref);\n  // The compact bar takes over once the large title has scrolled under it.\n  const collapseAt = headerHeight - top - barHeight;\n\n  const artwork = useAnimatedStyle(() => {\n    const y = offset.get();\n    if (y < 0) {\n      // Pulled down (iOS bounce): stretch from the top edge.\n      const scale = 1 + -y / headerHeight;\n      return { transform: [{ translateY: y / 2 }, { scale }] };\n    }\n    return { transform: [{ translateY: reduced ? -y : -y * 0.5 }] };\n  });\n\n  const largeTitle = useAnimatedStyle(() => {\n    const y = offset.get();\n    return {\n      opacity: interpolate(y, [0, collapseAt * 0.7], [1, 0], Extrapolation.CLAMP),\n      transform: [\n        {\n          translateY: reduced\n            ? 0\n            : interpolate(y, [-100, 0, collapseAt], [24, 0, -24], Extrapolation.CLAMP),\n        },\n      ],\n    };\n  });\n\n  const bar = useAnimatedStyle(() => {\n    const t = interpolate(offset.get(), [collapseAt - 24, collapseAt], [0, 1], Extrapolation.CLAMP);\n    return { opacity: t };\n  });\n  const barTitle = useAnimatedStyle(() => {\n    const t = interpolate(\n      offset.get(),\n      [collapseAt - 8, collapseAt + 16],\n      [0, 1],\n      Extrapolation.CLAMP,\n    );\n    return { opacity: t, transform: [{ translateY: reduced ? 0 : (1 - t) * 8 }] };\n  });\n\n  return (\n    <View style={[styles.root, { backgroundColor: theme.colors.background }, style]}>\n      <Animated.View\n        pointerEvents=\"none\"\n        style={[styles.artwork, { height: headerHeight }, artwork]}\n        importantForAccessibility=\"no-hide-descendants\"\n        accessibilityElementsHidden\n      >\n        {header}\n      </Animated.View>\n\n      <Animated.ScrollView\n        ref={ref}\n        scrollEventThrottle={16}\n        showsVerticalScrollIndicator={false}\n        // Lets it scroll when embedded in another scroll view on Android.\n        nestedScrollEnabled\n        contentContainerStyle={contentContainerStyle}\n      >\n        <View style={[styles.hero, { height: headerHeight, padding: theme.space[5] }]}>\n          <Animated.View style={largeTitle}>\n            <Text\n              accessibilityRole=\"header\"\n              style={{\n                color: theme.colors.text,\n                fontSize: theme.font.size['3xl'],\n                fontWeight: theme.font.weight.heavy,\n                letterSpacing: -1,\n              }}\n            >\n              {title}\n            </Text>\n            {subtitle ? (\n              <Text\n                style={{\n                  color: theme.colors.textMuted,\n                  fontSize: theme.font.size.md,\n                  marginTop: 4,\n                }}\n              >\n                {subtitle}\n              </Text>\n            ) : null}\n          </Animated.View>\n        </View>\n        <View style={{ backgroundColor: theme.colors.background }}>{children}</View>\n      </Animated.ScrollView>\n\n      <Animated.View\n        pointerEvents=\"box-none\"\n        style={[\n          styles.bar,\n          {\n            paddingTop: top,\n            height: top + barHeight,\n            backgroundColor: theme.colors.surface,\n            borderColor: theme.colors.border,\n          },\n          bar,\n        ]}\n      >\n        <Animated.Text\n          numberOfLines={1}\n          importantForAccessibility=\"no-hide-descendants\"\n          accessibilityElementsHidden\n          style={[\n            styles.barTitle,\n            {\n              color: theme.colors.text,\n              fontSize: theme.font.size.md,\n              fontWeight: theme.font.weight.bold,\n            },\n            barTitle,\n          ]}\n        >\n          {title}\n        </Animated.Text>\n      </Animated.View>\n      {barAccessory ? (\n        <View style={[styles.accessory, { top, height: barHeight, right: theme.space[3] }]}>\n          {barAccessory}\n        </View>\n      ) : null}\n    </View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  root: { flex: 1, overflow: 'hidden' },\n  artwork: { position: 'absolute', top: 0, left: 0, right: 0, overflow: 'hidden' },\n  hero: { justifyContent: 'flex-end' },\n  bar: {\n    position: 'absolute',\n    top: 0,\n    left: 0,\n    right: 0,\n    justifyContent: 'center',\n    borderBottomWidth: StyleSheet.hairlineWidth,\n  },\n  barTitle: { textAlign: 'center', paddingHorizontal: 64 },\n  accessory: { position: 'absolute', justifyContent: 'center' },\n});\n"
    }
  ],
  "meta": {
    "version": "1.0.0",
    "kind": "component",
    "platforms": [
      "ios",
      "android",
      "web"
    ],
    "category": "navigation",
    "web": "live",
    "perf": {
      "budget": "The scroll offset is read on the UI thread; four style worklets (artwork, title, bar, bar title) run per scroll frame. No JS work while scrolling.",
      "measurements": [
        {
          "device": "Pixel 9 emulator, API 35",
          "kind": "emulator",
          "date": "2026-10-12",
          "build": "release",
          "frameMs": 18,
          "p90Ms": 25,
          "baselineMs": 20,
          "jankyPercent": 8.2,
          "atRest": "idle",
          "memoryMb": 148
        }
      ]
    },
    "a11y": {
      "reducedMotion": "The artwork scrolls with the content instead of at half speed; titles fade without moving.",
      "screenReader": "The large title is a header; the compact bar's copy of it is hidden to avoid reading it twice. Artwork is decorative.",
      "touchTarget": "Not interactive itself; barAccessory keeps its own targets."
    },
    "requires": "React Native New Architecture (Reanimated 4)",
    "guide": "## Usage\n\n```tsx\nimport { ParallaxHeader } from '@/components/kinetik/parallax-header';\n\n<ParallaxHeader\n  title=\"Long Exposure\"\n  subtitle=\"Lumen Fields · 10 tracks\"\n  header={<Image source={cover} style={StyleSheet.absoluteFill} />}\n>\n  <TrackList />\n</ParallaxHeader>;\n```\n\n`ParallaxHeader` is the scroll view: put the screen's content inside it. The artwork fills `headerHeight` and should cover its area (`StyleSheet.absoluteFill`).\n\n## When to use\n\n- Detail screens led by an image: albums, places, profiles, products.\n- Screens where the title should stay reachable after the hero scrolls away.\n\n## When not to use\n\n- Lists without a meaningful hero; a plain header is quieter.\n- Screens that already use a native large-title navigation bar. Pick one.\n\n## Notes\n\n- The stretch on pull-down needs bounce scrolling (iOS); Android scrolls without it.\n- The compact bar starts below `topInset` (the safe-area top by default). Pass `0` when the component is not at the top of the screen.\n- Use `barAccessory` for a share or close button that stays visible.\n"
  },
  "docs": "Prefer `npx kinetik-ui add parallax-header`: it installs dependencies with expo install, so versions match your Expo SDK."
}
