{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notify",
  "title": "Notifications",
  "description": "in-app banner now (`delay: 0`), real OS notification when scheduled",
  "dependencies": [
    "expo-notifications@~57.0.20",
    "lucide-react-native@^1.21.0",
    "react-native-safe-area-context@~5.7.0"
  ],
  "registryDependencies": [
    "@kit-native/icon",
    "@kit-native/lib-haptics",
    "@kit-native/lib-native-context",
    "@kit-native/lib-reduced-motion",
    "@kit-native/lib-utils",
    "@kit-native/text",
    "@kit-native/toast"
  ],
  "files": [
    {
      "path": "components/kit/notify.tsx",
      "content": "import { 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 { useMotion } from '@/lib/reduced-motion';\nimport { BellIcon, type LucideIcon } from 'lucide-react-native';\nimport * as React from 'react';\nimport { Animated, Easing, Pressable, View } from 'react-native';\nimport { useSafeAreaInsets } from 'react-native-safe-area-context';\n\nexport type NotifyOptions = {\n  title: string;\n  body?: string;\n  /** Seconds from now. 0 (default) shows the in-app banner immediately. */\n  delay?: number;\n  icon?: LucideIcon;\n};\n\ntype BannerState = { id: number; title: string; body?: string; icon: LucideIcon };\n\ntype NotifyApi = {\n  notify: (options: NotifyOptions) => Promise<void>;\n  cancelAll: () => Promise<void>;\n  /** True when no OS notification will actually fire. */\n  simulated: boolean;\n};\n\nconst NotifyContext = React.createContext<NotifyApi | null>(null);\n\n/**\n * Local notifications: an in-app banner now, a real OS notification when scheduled.\n *\n * `delay: 0` always draws the banner — identical on web, iOS and Android, and what a\n * demo actually needs (iOS suppresses foreground local notifications anyway).\n * `delay > 0` also schedules the real thing wherever that works.\n */\nexport function useNotify(): NotifyApi {\n  const ctx = React.useContext(NotifyContext);\n  if (!ctx) throw new Error('useNotify must be used inside <NotifyProvider>');\n  return ctx;\n}\n\nexport function NotifyProvider({ children }: { children: React.ReactNode }) {\n  const [banner, setBanner] = React.useState<BannerState | null>(null);\n  const idRef = React.useRef(0);\n  const timers = React.useRef<ReturnType<typeof setTimeout>[]>([]);\n  const { cap, request } = useCapability('notify');\n  const toast = useToast();\n\n  const dismiss = React.useCallback(() => setBanner(null), []);\n\n  const showBanner = React.useCallback((options: NotifyOptions) => {\n    haptic('light');\n    setBanner({\n      id: ++idRef.current,\n      title: options.title,\n      body: options.body,\n      icon: options.icon ?? BellIcon,\n    });\n  }, []);\n\n  /** true when the OS accepted it. false means the caller must fall back. */\n  const schedule = React.useCallback(\n    async (options: NotifyOptions): Promise<boolean> => {\n      const settled = cap.state === 'ready' ? cap : await request();\n      if (settled.simulated || settled.state !== 'ready') return false;\n      try {\n        // Lazily required: importing expo-notifications warns and registers a push-token\n        // side effect under Expo Go, which would fire in every prototype session.\n        // eslint-disable-next-line @typescript-eslint/no-require-imports\n        const Notifications = require('expo-notifications') as typeof import('expo-notifications');\n        Notifications.setNotificationHandler({\n          handleNotification: async () => ({\n            shouldShowBanner: true,\n            shouldShowList: true,\n            shouldPlaySound: false,\n            shouldSetBadge: false,\n          }),\n        });\n        await Notifications.scheduleNotificationAsync({\n          content: { title: options.title, body: options.body },\n          trigger: { type: 'timeInterval', seconds: Math.max(1, options.delay ?? 1), repeats: false } as never,\n        });\n        return true;\n      } catch {\n        return false;\n      }\n    },\n    [cap, request]\n  );\n\n  const notify = React.useCallback(\n    async (options: NotifyOptions) => {\n      const delay = options.delay ?? 0;\n      if (delay === 0) {\n        showBanner(options);\n        return;\n      }\n      if (await schedule(options)) return;\n      // The OS will not deliver it — alerts are off, or this runtime cannot schedule.\n      // Fall back to the in-app banner after the same delay so the flow still lands.\n      toast.info(`Alerts are off — showing it in the app in ${delay}s`);\n      timers.current.push(setTimeout(() => showBanner(options), delay * 1000));\n    },\n    [schedule, showBanner, toast]\n  );\n\n  const cancelAll = React.useCallback(async () => {\n    setBanner(null);\n    timers.current.forEach(clearTimeout);\n    timers.current = [];\n    if (cap.simulated) return;\n    try {\n      // eslint-disable-next-line @typescript-eslint/no-require-imports\n      const Notifications = require('expo-notifications') as typeof import('expo-notifications');\n      await Notifications.cancelAllScheduledNotificationsAsync();\n    } catch {\n      /* nothing scheduled */\n    }\n  }, [cap.simulated]);\n\n  const api = React.useMemo<NotifyApi>(\n    () => ({ notify, cancelAll, simulated: cap.simulated }),\n    [notify, cancelAll, cap.simulated]\n  );\n\n  return (\n    <NotifyContext.Provider value={api}>\n      {children}\n      <NotificationBanner banner={banner} onDismiss={dismiss} />\n    </NotifyContext.Provider>\n  );\n}\n\n/** The iOS-style notification card. Slides in under the safe area, auto-dismisses, tap to close. */\nfunction NotificationBanner({ banner, onDismiss }: { banner: BannerState | null; onDismiss: () => void }) {\n  const insets = useSafeAreaInsets();\n  const [slide] = React.useState(() => new Animated.Value(0));\n  const motion = useMotion();\n\n  React.useEffect(() => {\n    if (!banner) return;\n    Animated.timing(slide, { toValue: 1, duration: motion(260), easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();\n    const timer = setTimeout(() => {\n      Animated.timing(slide, { toValue: 0, duration: motion(200), easing: Easing.in(Easing.cubic), useNativeDriver: true }).start(\n        ({ finished }) => {\n          if (finished) onDismiss();\n        }\n      );\n    }, 4000);\n    return () => clearTimeout(timer);\n  }, [banner, slide, onDismiss, motion]);\n\n  if (!banner) return null;\n\n  return (\n    <View\n      pointerEvents=\"box-none\"\n      accessibilityLiveRegion=\"polite\"\n      className=\"absolute left-0 right-0\"\n      style={{ top: Math.max(insets.top, 12) }}>\n      <Animated.View\n        style={{\n          opacity: slide,\n          transform: [{ translateY: slide.interpolate({ inputRange: [0, 1], outputRange: [-24, 0] }) }],\n        }}\n        className=\"px-4\">\n        <Pressable\n          onPress={onDismiss}\n          accessibilityRole=\"button\"\n          accessibilityLabel={`Notification: ${banner.title}. Tap to dismiss.`}\n          className={cn('bg-card border-border flex-row items-center gap-3 rounded-2xl border p-3 shadow-lg shadow-black/20 active:opacity-90')}>\n          <View className=\"bg-muted size-9 items-center justify-center rounded-lg\">\n            <Icon as={banner.icon} size={18} className=\"text-foreground\" />\n          </View>\n          <View className=\"flex-1 gap-0.5\">\n            <View className=\"flex-row items-start justify-between gap-2\">\n              <Text className=\"flex-1 text-sm font-semibold\" numberOfLines={1}>\n                {banner.title}\n              </Text>\n              <Text className=\"text-muted-foreground text-xs\">now</Text>\n            </View>\n            {banner.body ? (\n              <Text className=\"text-muted-foreground text-sm leading-5\" numberOfLines={2}>\n                {banner.body}\n              </Text>\n            ) : null}\n          </View>\n        </Pressable>\n      </Animated.View>\n    </View>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/kit/notify.tsx"
    }
  ],
  "meta": {
    "exports": [
      "useNotify",
      "NotifyProvider"
    ],
    "api": "const { notify } = useNotify(); notify({ title, body?, delay?, icon? })"
  },
  "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"
}