{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "profile-hero",
  "type": "registry:block",
  "title": "Profile Hero",
  "description": "A profile screen: parallax cover, follow button with a rolling count, liquid tabs and loading skeletons.",
  "categories": [
    "screen"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://kinetik-ui.dev/r/kinetik-tokens.json",
    "https://kinetik-ui.dev/r/grain-drift.json",
    "https://kinetik-ui.dev/r/liquid-tabs.json",
    "https://kinetik-ui.dev/r/odometer.json",
    "https://kinetik-ui.dev/r/parallax-header.json",
    "https://kinetik-ui.dev/r/press-jelly.json",
    "https://kinetik-ui.dev/r/skeleton-sweep.json",
    "https://kinetik-ui.dev/r/toast-stack.json"
  ],
  "files": [
    {
      "path": "src/components/kinetik/screens/profile-hero.tsx",
      "type": "registry:file",
      "target": "~/components/kinetik/screens/profile-hero.tsx",
      "content": "import { useEffect, useState, type ReactNode } from 'react';\nimport { StyleSheet, Text, View } from 'react-native';\n\nimport { GrainDrift } from '../grain-drift';\nimport { LiquidTabs } from '../liquid-tabs';\nimport { Odometer } from '../odometer';\nimport { ParallaxHeader } from '../parallax-header';\nimport { PressJelly } from '../press-jelly';\nimport { Skeleton, SkeletonGroup } from '../skeleton-sweep';\nimport { ToastProvider, useToast } from '../toast-stack';\nimport { gradient } from '../../../lib/kinetik/tokens/gradient';\nimport { useKinetikTheme } from '../../../lib/kinetik/tokens/theme';\nimport { withAlpha } from '../../../lib/kinetik/tokens/tokens';\n\nexport type ProfileTabKey = 'posts' | 'liked' | 'about';\n\nexport type Profile = {\n  name: string;\n  handle: string;\n  bio?: string;\n  stats: { posts: number; followers: number; following: number };\n  /** Label/value rows for the About tab. */\n  about?: readonly { label: string; value: string }[];\n};\n\nexport type ProfileHeroProps = {\n  profile?: Profile;\n  following?: boolean;\n  onFollowChange?: (following: boolean) => void;\n  onMessage?: () => void;\n  /** Load a tab's content. Skeletons show until the promise settles. */\n  loadTab?: (tab: ProfileTabKey) => Promise<void>;\n  /** Replace a tab's content. Defaults to a gradient grid (posts, liked) and the about rows. */\n  renderTab?: (tab: ProfileTabKey) => ReactNode;\n  /** Top safe-area inset for the collapsed bar. Defaults to the device inset. */\n  topInset?: number;\n};\n\nconst DEFAULT_PROFILE: Profile = {\n  name: 'Mira Okafor',\n  handle: '@mira',\n  bio: 'Motion designer in Lisbon. Making interfaces feel like they were built with hands.',\n  stats: { posts: 128, followers: 24816, following: 312 },\n  about: [\n    { label: 'Based in', value: 'Lisbon, Portugal' },\n    { label: 'Studio', value: 'Field Notes' },\n    { label: 'Joined', value: 'March 2021' },\n    { label: 'Website', value: 'mira.studio' },\n  ],\n};\n\nconst TABS = [\n  { key: 'posts', label: 'Posts' },\n  { key: 'liked', label: 'Liked' },\n  { key: 'about', label: 'About' },\n] as const;\n\n/**\n * A profile screen: a drifting cover that parallaxes away, a follow button\n * whose count rolls, liquid tabs and skeletons while a tab loads.\n */\nexport function ProfileHero(props: ProfileHeroProps) {\n  return (\n    <ToastProvider placement=\"bottom\">\n      <ProfileScreen {...props} />\n    </ToastProvider>\n  );\n}\n\nfunction ProfileScreen({\n  profile = DEFAULT_PROFILE,\n  following: followingProp,\n  onFollowChange,\n  onMessage,\n  loadTab,\n  renderTab,\n  topInset,\n}: ProfileHeroProps) {\n  const { colors, space, font } = useKinetikTheme();\n  const toast = useToast();\n  const [followingState, setFollowing] = useState(false);\n  const following = followingProp ?? followingState;\n  const [tab, setTab] = useState<ProfileTabKey>('posts');\n  const [loading, setLoading] = useState<ProfileTabKey | null>(loadTab ? 'posts' : null);\n\n  useEffect(() => {\n    if (!loadTab) return;\n    let live = true;\n    loadTab(tab)\n      .catch(() => {})\n      .finally(() => live && setLoading(null));\n    return () => {\n      live = false;\n    };\n  }, [tab, loadTab]);\n\n  const changeTab = (key: string) => {\n    if (loadTab) setLoading(key as ProfileTabKey);\n    setTab(key as ProfileTabKey);\n  };\n\n  const first = profile.name.split(' ')[0] ?? profile.name;\n  const setFollow = (next: boolean) => {\n    setFollowing(next);\n    onFollowChange?.(next);\n  };\n  const toggleFollow = () => {\n    const next = !following;\n    setFollow(next);\n    if (next) {\n      toast.show({\n        title: `Following ${first}`,\n        description: 'Their posts will show up in your feed.',\n        tone: 'success',\n        action: { label: 'Undo', onPress: () => setFollow(false) },\n      });\n    }\n  };\n\n  const stats = [\n    { label: 'Posts', value: profile.stats.posts },\n    { label: 'Followers', value: profile.stats.followers + (following ? 1 : 0) },\n    { label: 'Following', value: profile.stats.following },\n  ];\n\n  return (\n    <ParallaxHeader\n      title={profile.name}\n      subtitle={profile.handle}\n      headerHeight={320}\n      topInset={topInset}\n      header={\n        <GrainDrift style={StyleSheet.absoluteFill} grain={0.4}>\n          <View\n            style={[\n              StyleSheet.absoluteFill,\n              gradient(\n                `linear-gradient(180deg, ${withAlpha(colors.background, 0)} 35%, ${withAlpha(colors.background, 0.7)} 75%, ${colors.background} 100%)`,\n              ),\n            ]}\n          />\n        </GrainDrift>\n      }\n    >\n      <View style={{ paddingHorizontal: space[5], gap: space[5], paddingBottom: space[10] }}>\n        {profile.bio ? (\n          <Text style={{ color: colors.textMuted, fontSize: font.size.md, lineHeight: 23 }}>\n            {profile.bio}\n          </Text>\n        ) : null}\n\n        <View style={[styles.row, { gap: space[3] }]}>\n          <View style={styles.fill}>\n            <PressJelly\n              block\n              variant={following ? 'surface' : 'ember'}\n              onPress={toggleFollow}\n              accessibilityLabel={following ? `Following ${first}` : `Follow ${first}`}\n              accessibilityHint={following ? 'Unfollows' : undefined}\n            >\n              {following ? 'Following' : 'Follow'}\n            </PressJelly>\n          </View>\n          {onMessage ? (\n            <View style={styles.fill}>\n              <PressJelly block variant=\"surface\" onPress={onMessage}>\n                Message\n              </PressJelly>\n            </View>\n          ) : null}\n        </View>\n\n        <View\n          style={[\n            styles.row,\n            styles.stats,\n            { borderColor: colors.border, paddingVertical: space[4] },\n          ]}\n        >\n          {stats.map((s) => (\n            <View\n              key={s.label}\n              accessible\n              accessibilityLabel={`${s.value.toLocaleString('en-US')} ${s.label.toLowerCase()}`}\n              style={styles.stat}\n            >\n              <Odometer\n                value={s.value}\n                locale=\"en-US\"\n                containerStyle={styles.statValue}\n                style={{\n                  color: colors.text,\n                  fontSize: font.size.xl,\n                  fontWeight: font.weight.heavy,\n                }}\n              />\n              <Text style={{ color: colors.textMuted, fontSize: font.size.xs, marginTop: 2 }}>\n                {s.label}\n              </Text>\n            </View>\n          ))}\n        </View>\n\n        <LiquidTabs\n          accessibilityLabel=\"Profile sections\"\n          tabs={TABS}\n          value={tab}\n          onChange={changeTab}\n        />\n\n        {loading === tab ? (\n          <TabSkeleton tab={tab} />\n        ) : renderTab ? (\n          renderTab(tab)\n        ) : tab === 'about' ? (\n          <About rows={profile.about ?? []} />\n        ) : (\n          <Grid seed={tab === 'posts' ? 0 : 5} />\n        )}\n      </View>\n    </ParallaxHeader>\n  );\n}\n\nfunction TabSkeleton({ tab }: { tab: ProfileTabKey }) {\n  const { space, radius } = useKinetikTheme();\n  if (tab === 'about') {\n    return (\n      <SkeletonGroup accessibilityLabel=\"Loading details\" style={{ gap: space[4] }}>\n        {[0, 1, 2, 3].map((i) => (\n          <View key={i} style={{ gap: space[1.5] }}>\n            <Skeleton width=\"30%\" height={12} />\n            <Skeleton width=\"60%\" height={16} />\n          </View>\n        ))}\n      </SkeletonGroup>\n    );\n  }\n  return (\n    <SkeletonGroup accessibilityLabel=\"Loading posts\" style={{ gap: space[2] }}>\n      {ROWS.map((row) => (\n        <View key={row} style={[styles.gridRow, { gap: space[2] }]}>\n          {COLS.map((col) => (\n            <View key={col} style={styles.cell}>\n              <Skeleton height=\"100%\" radius={radius.md} />\n            </View>\n          ))}\n        </View>\n      ))}\n    </SkeletonGroup>\n  );\n}\n\n// Explicit rows: Yoga leaves aspectRatio cells with a percentage width at zero\n// height inside a wrapping row, so the grid is built from flex rows instead.\nconst ROWS = [0, 1, 2];\nconst COLS = [0, 1, 2];\n\nfunction Grid({ seed }: { seed: number }) {\n  const { colors, space, radius } = useKinetikTheme();\n  const tones = [colors.ember, colors.ice, colors.textMuted, colors.textSubtle];\n  return (\n    <View style={{ gap: space[2] }}>\n      {ROWS.map((row) => (\n        <View key={row} style={[styles.gridRow, { gap: space[2] }]}>\n          {COLS.map((col) => {\n            const i = row * COLS.length + col;\n            const a = tones[(i + seed) % tones.length]!;\n            const b = tones[(i * 3 + seed + 1) % tones.length]!;\n            return (\n              <View\n                key={col}\n                accessible\n                accessibilityRole=\"image\"\n                accessibilityLabel={`Post ${i + 1}`}\n                style={[\n                  styles.cell,\n                  { borderRadius: radius.md, overflow: 'hidden' },\n                  gradient(\n                    `linear-gradient(${35 + i * 40}deg, ${withAlpha(a, 0.85)} 0%, ${withAlpha(b, 0.35)} 100%)`,\n                  ),\n                ]}\n              />\n            );\n          })}\n        </View>\n      ))}\n    </View>\n  );\n}\n\nfunction About({ rows }: { rows: readonly { label: string; value: string }[] }) {\n  const { colors, space, font } = useKinetikTheme();\n  return (\n    <View>\n      {rows.map((r, i) => (\n        <View\n          key={r.label}\n          accessible\n          style={[\n            styles.aboutRow,\n            {\n              paddingVertical: space[3],\n              borderTopWidth: i === 0 ? 0 : StyleSheet.hairlineWidth,\n              borderColor: colors.border,\n            },\n          ]}\n        >\n          <Text style={{ color: colors.textMuted, fontSize: font.size.sm }}>{r.label}</Text>\n          <Text style={{ color: colors.text, fontSize: font.size.md }}>{r.value}</Text>\n        </View>\n      ))}\n    </View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  fill: { flex: 1 },\n  row: { flexDirection: 'row', alignItems: 'center' },\n  stats: { borderTopWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth },\n  stat: { flex: 1, alignItems: 'center' },\n  statValue: { alignSelf: 'center' },\n  gridRow: { flexDirection: 'row' },\n  cell: { flex: 1, aspectRatio: 1 },\n  aboutRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },\n});\n"
    }
  ],
  "meta": {
    "version": "1.0.0",
    "kind": "template",
    "platforms": [
      "ios",
      "android",
      "web"
    ],
    "category": "screen",
    "web": "live",
    "perf": {
      "budget": "The cover shader runs while the header is on screen; the parallax, collapsing bar and tab indicator are scroll- and spring-driven worklets. The grid is plain views.",
      "measurements": []
    },
    "a11y": {
      "reducedMotion": "The cover holds still, the header scrolls without parallax, counts change without rolling and skeletons do not sweep (handled by each component).",
      "screenReader": "The name is a header; each stat reads as one value (\"24,817 followers\"); tabs are a tab list; skeletons announce \"Loading posts\". Following shows a toast with an Undo action.",
      "touchTarget": "Buttons and tabs are at least 44 pt."
    },
    "requires": "React Native New Architecture (Reanimated 4)",
    "guide": "## Usage\n\n```tsx\nimport { ProfileHero } from '@/components/kinetik/screens/profile-hero';\n\n<ProfileHero\n  profile={{\n    name: user.name,\n    handle: `@${user.username}`,\n    bio: user.bio,\n    stats: { posts: user.posts, followers: user.followers, following: user.following },\n    about: [{ label: 'Based in', value: user.city }],\n  }}\n  following={isFollowing}\n  onFollowChange={(next) => (next ? follow(user.id) : unfollow(user.id))}\n  onMessage={() => router.push(`/messages/${user.id}`)}\n  loadTab={(tab) => queryClient.prefetchQuery(profileTabQuery(user.id, tab))}\n  renderTab={(tab) => <ProfileTab userId={user.id} tab={tab} />}\n/>;\n```\n\n## When to use\n\n- A person's or a brand's page with a cover, a primary action and a few sections of content.\n- When the content below the header scrolls; the parallax and collapsing bar need room to work.\n\n## When not to use\n\n- Your own account settings. Use a plain grouped list.\n- Pages with more than three or four sections; Liquid Tabs is for short peer views.\n\n## Notes\n\n- Includes its own `ToastProvider` (bottom) for the follow confirmation and its Undo action.\n- Pass `following` to control the button from your data; without it the screen keeps its own state.\n- The follower count adds one while following, so the rolling number confirms the tap.\n"
  },
  "docs": "Prefer `npx kinetik-ui add profile-hero`: it installs dependencies with expo install, so versions match your Expo SDK."
}
