{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "photo-capture",
  "title": "Camera",
  "description": "camera + library · falls back to a seeded `Placeholder`",
  "dependencies": [
    "expo-image-picker@~57.0.19",
    "lucide-react-native@^1.21.0"
  ],
  "registryDependencies": [
    "@kit-native/icon",
    "@kit-native/lib-haptics",
    "@kit-native/lib-native-context",
    "@kit-native/lib-utils",
    "@kit-native/placeholder",
    "@kit-native/sheet",
    "@kit-native/text",
    "@kit-native/toast"
  ],
  "files": [
    {
      "path": "components/kit/photo-capture.tsx",
      "content": "import { Placeholder } from '@/components/kit/placeholder';\nimport { ActionSheet } from '@/components/kit/sheet';\nimport { useToast } from '@/components/kit/toast';\nimport { Icon } from '@/components/ui/icon';\nimport { Text } from '@/components/ui/text';\nimport { haptic } from '@/lib/haptics';\nimport { useCapability } from '@/lib/native-context';\nimport { cn } from '@/lib/utils';\nimport * as ImagePicker from 'expo-image-picker';\nimport { CameraIcon, ImageIcon, XIcon } from 'lucide-react-native';\nimport * as React from 'react';\nimport { Image, Pressable, View } from 'react-native';\n\nexport type PhotoSource = 'camera' | 'library';\n\ntype Props = {\n  /** Current photo URI. Omit to let the component own it. */\n  value?: string | null;\n  onChange?: (uri: string | null) => void;\n  /** Where a photo may come from. Default both. */\n  sources?: PhotoSource[];\n  /** `avatar` is a circle, `wide` is 16:9. Default `tile` (4:3). */\n  shape?: 'tile' | 'avatar' | 'wide';\n  /** Copy inside the empty frame. */\n  label?: string;\n  /** Seed for the simulated photo. Defaults to `label`. */\n  seed?: string;\n  removable?: boolean;\n  /** Force the simulated path — demos, screenshots. */\n  simulate?: boolean;\n  className?: string;\n};\n\n/** A simulated photo is a sentinel URI, not a file — the render branch draws art instead. */\nconst SIM = 'kit-placeholder:';\n\nconst RATIO = { tile: 4 / 3, avatar: 1, wide: 16 / 9 };\n\n/**\n * Tap to add a photo, from the camera or the library.\n *\n * Where there is no camera — the web preview, the simulator, a declined permission —\n * it drops to generated art so the screen still reads as filled.\n */\nexport function PhotoCapture({\n  value,\n  onChange,\n  sources = ['camera', 'library'],\n  shape = 'tile',\n  label = 'Add a photo',\n  seed,\n  removable = true,\n  simulate,\n  className,\n}: Props) {\n  const [own, setOwn] = React.useState<string | null>(null);\n  const [sheetOpen, setSheetOpen] = React.useState(false);\n  const [simCount, setSimCount] = React.useState(0);\n  const toast = useToast();\n  const camera = useCapability('camera', simulate);\n  const photos = useCapability('photos', simulate);\n\n  const uri = value !== undefined ? value : own;\n  const set = (next: string | null) => {\n    if (value === undefined) setOwn(next);\n    onChange?.(next);\n  };\n\n  const simulateShot = (note?: string) => {\n    haptic('success');\n    setSimCount((n) => n + 1);\n    set(`${SIM}${seed ?? label}-${simCount}`);\n    if (note) toast.info(note);\n  };\n\n  const pick = async (source: PhotoSource) => {\n    const capability = source === 'camera' ? camera : photos;\n    if (capability.cap.simulated) {\n      simulateShot(capability.cap.note);\n      return;\n    }\n    const settled = await capability.request();\n    if (settled.simulated) {\n      simulateShot(source === 'camera' ? 'Using a sample photo — camera access is off' : 'Using a sample photo — photo access is off');\n      return;\n    }\n    try {\n      const result =\n        source === 'camera'\n          ? await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 0.7 })\n          : await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 0.7 });\n      if (result.canceled || !result.assets?.[0]) return;\n      haptic('success');\n      set(result.assets[0].uri);\n    } catch {\n      simulateShot('Could not open the picker — using a sample photo');\n    }\n  };\n\n  const open = () => {\n    haptic('selection');\n    if (sources.length === 1) {\n      void pick(sources[0]);\n      return;\n    }\n    setSheetOpen(true);\n  };\n\n  const frame = shape === 'avatar' ? 'rounded-full' : 'rounded-xl';\n  const simulated = !!uri && uri.startsWith(SIM);\n\n  return (\n    <View className={cn('gap-2', className)}>\n      <Pressable\n        onPress={open}\n        accessibilityRole=\"button\"\n        accessibilityLabel={uri ? 'Change photo' : label}\n        className={cn('bg-muted w-full items-center justify-center overflow-hidden active:opacity-80', frame, !uri && 'border-border border-2 border-dashed')}\n        style={{ aspectRatio: RATIO[shape] }}>\n        {simulated ? (\n          <Placeholder seed={uri.slice(SIM.length)} ratio={RATIO[shape]} className={cn(frame, 'h-full')} />\n        ) : uri ? (\n          <Image source={{ uri }} className=\"h-full w-full\" resizeMode=\"cover\" accessibilityIgnoresInvertColors />\n        ) : (\n          <View className=\"items-center gap-2 p-4\">\n            <Icon as={CameraIcon} size={24} className=\"text-muted-foreground\" />\n            <Text className=\"text-muted-foreground text-center text-sm font-medium\">{label}</Text>\n          </View>\n        )}\n      </Pressable>\n\n      {uri && removable ? (\n        <Pressable\n          onPress={() => {\n            haptic('light');\n            set(null);\n          }}\n          accessibilityRole=\"button\"\n          accessibilityLabel=\"Remove photo\"\n          className=\"min-h-11 flex-row items-center gap-1 self-start\">\n          <Icon as={XIcon} size={14} className=\"text-muted-foreground\" />\n          <Text className=\"text-muted-foreground text-sm font-medium\">Remove</Text>\n        </Pressable>\n      ) : null}\n\n      <ActionSheet\n        open={sheetOpen}\n        onClose={() => setSheetOpen(false)}\n        title=\"Add a photo\"\n        items={[\n          { label: 'Take a photo', icon: CameraIcon, onPress: () => void pick('camera') },\n          { label: 'Choose from library', icon: ImageIcon, onPress: () => void pick('library') },\n          ...(uri && removable ? [{ label: 'Remove photo', icon: XIcon, destructive: true, onPress: () => set(null) }] : []),\n        ]}\n      />\n    </View>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/kit/photo-capture.tsx"
    }
  ],
  "meta": {
    "exports": [
      "PhotoCapture"
    ],
    "api": "<PhotoCapture value? onChange? sources? shape=\"tile|avatar|wide\" label? seed? simulate? />"
  },
  "docs": "Run `npx expo install --fix` afterwards so Expo SDK packages match your SDK. Needs the kit theme (`@kit-native/theme`) and `KitThemeProvider` from `@kit-native/lib-theme-context` at the root.",
  "categories": [
    "native"
  ],
  "type": "registry:component"
}