{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-suggested-prompts",
  "title": "AI Suggested Prompts",
  "description": "Display suggested prompts with categories and search.",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "input-group",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/ai/ai-suggested-prompts.tsx",
      "content": "\"use client\";\n\nimport {\n  Code,\n  FileText,\n  Lightbulb,\n  Search,\n  Sparkles,\n  TrendingUp,\n} from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/registry/new-york/ui/badge\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/new-york/ui/card\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\n\nexport interface SuggestedPrompt {\n  id: string;\n  title: string;\n  prompt: string;\n  category: string;\n  icon?: React.ComponentType<{ className?: string }>;\n  description?: string;\n  isRecent?: boolean;\n  isPopular?: boolean;\n  usageCount?: number;\n}\n\nexport interface AISuggestedPromptsProps {\n  prompts?: SuggestedPrompt[];\n  categories?: string[];\n  onSelect?: (prompt: SuggestedPrompt) => void;\n  onSearch?: (query: string) => void;\n  className?: string;\n  showSearch?: boolean;\n  showCategories?: boolean;\n  maxDisplay?: number;\n  isLoading?: boolean;\n}\n\nconst defaultCategories = [\n  \"All\",\n  \"Writing\",\n  \"Code\",\n  \"Analysis\",\n  \"Creative\",\n  \"Research\",\n];\n\nconst categoryIcons: Record<\n  string,\n  React.ComponentType<{ className?: string }>\n> = {\n  Writing: FileText,\n  Code,\n  Analysis: TrendingUp,\n  Creative: Sparkles,\n  Research: Lightbulb,\n};\n\ninterface ComponentHeaderProps {\n  title: string;\n  description: string;\n}\n\nfunction ComponentHeader({ title, description }: ComponentHeaderProps) {\n  return (\n    <div className=\"flex flex-col gap-1\">\n      <CardTitle>{title}</CardTitle>\n      <CardDescription>{description}</CardDescription>\n    </div>\n  );\n}\n\ninterface PromptSearchProps {\n  value: string;\n  onChange: (value: string) => void;\n  placeholder?: string;\n  resultCount?: number;\n}\n\nfunction PromptSearch({\n  value,\n  onChange,\n  placeholder = \"Search prompts…\",\n  resultCount,\n}: PromptSearchProps) {\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (\n        (e.metaKey || e.ctrlKey) &&\n        e.key === \"k\" &&\n        !e.shiftKey &&\n        document.activeElement !== inputRef.current\n      ) {\n        e.preventDefault();\n        inputRef.current?.focus();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, []);\n\n  const handleClear = useCallback(() => {\n    onChange(\"\");\n    inputRef.current?.focus();\n  }, [onChange]);\n\n  return (\n    <div className=\"relative\">\n      <InputGroup>\n        <InputGroupAddon>\n          <Search aria-hidden=\"true\" className=\"size-4\" />\n        </InputGroupAddon>\n        <InputGroupInput\n          aria-label=\"Search prompts\"\n          onChange={(e) => onChange(e.target.value)}\n          onKeyDown={(e) => {\n            if (e.key === \"Escape\") {\n              handleClear();\n            }\n          }}\n          placeholder={placeholder}\n          ref={inputRef}\n          type=\"search\"\n          value={value}\n        />\n      </InputGroup>\n      {resultCount !== undefined && value && (\n        <div aria-live=\"polite\" className=\"sr-only\" role=\"status\">\n          {resultCount} result{resultCount !== 1 ? \"s\" : \"\"} found\n        </div>\n      )}\n    </div>\n  );\n}\n\ninterface CategoryFilterProps {\n  categories: string[];\n  selectedCategory: string;\n  onSelect: (category: string) => void;\n}\n\nfunction CategoryFilter({\n  categories,\n  selectedCategory,\n  onSelect,\n}: CategoryFilterProps) {\n  return (\n    <div\n      aria-label=\"Filter prompts by category\"\n      className=\"flex flex-wrap gap-2\"\n      role=\"tablist\"\n    >\n      {categories.map((category) => {\n        const Icon = categoryIcons[category];\n        const isSelected = selectedCategory === category;\n        return (\n          <Button\n            aria-label={`Filter by ${category}`}\n            aria-selected={isSelected}\n            className={cn(\n              \"min-h-[32px] gap-2 px-3 py-1.5 sm:h-auto sm:min-h-[10px]\",\n              isSelected\n                ? \"border border-border bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground\"\n                : \"bg-muted text-muted-foreground hover:bg-muted/80\"\n            )}\n            key={category}\n            onClick={() => onSelect(category)}\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            {Icon && <Icon aria-hidden=\"true\" className=\"size-3.5\" />}\n            <span className=\"text-xs\">{category}</span>\n          </Button>\n        );\n      })}\n    </div>\n  );\n}\n\ninterface EmptyStateProps {\n  type: \"no-prompts\" | \"no-results\";\n  searchQuery?: string;\n}\n\nfunction EmptyState({ type, searchQuery }: EmptyStateProps) {\n  if (type === \"no-prompts\") {\n    return (\n      <div className=\"flex flex-col items-center justify-center gap-4 py-12 text-center\">\n        <div className=\"flex size-12 items-center justify-center rounded-full bg-muted\">\n          <Lightbulb\n            aria-hidden=\"true\"\n            className=\"size-6 text-muted-foreground\"\n          />\n        </div>\n        <div className=\"flex flex-col gap-2\">\n          <p className=\"font-medium text-sm\">No prompts available</p>\n          <p className=\"text-muted-foreground text-sm\">\n            Prompts will appear here when they become available\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col items-center justify-center gap-4 py-12 text-center\">\n      <div className=\"flex size-12 items-center justify-center rounded-full bg-muted\">\n        <Search aria-hidden=\"true\" className=\"size-6 text-muted-foreground\" />\n      </div>\n      <div className=\"flex flex-col gap-2\">\n        <p className=\"font-medium text-sm\">No prompts match your search</p>\n        <p className=\"text-muted-foreground text-sm\">\n          Try adjusting your search terms or browse different categories\n        </p>\n        {searchQuery && (\n          <p className=\"text-muted-foreground text-xs\">\n            Searched for: &quot;{searchQuery}&quot;\n          </p>\n        )}\n      </div>\n    </div>\n  );\n}\n\ninterface PromptItemProps {\n  prompt: SuggestedPrompt;\n  isFocused: boolean;\n  index: number;\n  onSelect: (prompt: SuggestedPrompt) => void;\n  itemRef: (node: HTMLButtonElement | null) => void;\n}\n\nfunction PromptItem({\n  prompt,\n  isFocused,\n  index,\n  onSelect,\n  itemRef,\n}: PromptItemProps) {\n  const Icon = prompt.icon || categoryIcons[prompt.category] || Lightbulb;\n\n  const handleSelect = useCallback(() => {\n    onSelect(prompt);\n  }, [onSelect, prompt]);\n\n  return (\n    <button\n      aria-label={`Use prompt: ${prompt.title}`}\n      className={cn(\n        \"group flex min-h-[44px] flex-col gap-2 rounded-lg border bg-card p-4 text-left transition-colors\",\n        \"hover:border-primary hover:bg-primary/5\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n        isFocused && \"border-primary bg-primary/5\"\n      )}\n      data-prompt-index={index}\n      onClick={handleSelect}\n      ref={itemRef}\n      type=\"button\"\n    >\n      <div className=\"flex items-start gap-3\">\n        <div\n          aria-hidden=\"true\"\n          className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary\"\n        >\n          <Icon className=\"size-4\" />\n        </div>\n        <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n          <div className=\"flex flex-wrap items-center justify-between gap-2\">\n            <h3 className=\"font-medium text-sm\">{prompt.title}</h3>\n            <div className=\"flex shrink-0 gap-1\">\n              {prompt.isPopular && (\n                <Badge className=\"text-xs\" variant=\"default\">\n                  Popular\n                </Badge>\n              )}\n              {prompt.isRecent && (\n                <Badge className=\"text-xs\" variant=\"secondary\">\n                  Recent\n                </Badge>\n              )}\n            </div>\n          </div>\n          {prompt.description && (\n            <p className=\"line-clamp-2 text-muted-foreground text-xs\">\n              {prompt.description}\n            </p>\n          )}\n          {prompt.usageCount !== undefined && (\n            <p className=\"text-muted-foreground text-xs\">\n              Used {prompt.usageCount} time{prompt.usageCount !== 1 ? \"s\" : \"\"}\n            </p>\n          )}\n        </div>\n      </div>\n      {prompt.prompt && (\n        <>\n          <Separator className=\"my-1\" />\n          <p className=\"line-clamp-2 text-muted-foreground text-xs italic\">\n            &quot;{prompt.prompt}&quot;\n          </p>\n        </>\n      )}\n    </button>\n  );\n}\n\ninterface PromptListProps {\n  prompts: SuggestedPrompt[];\n  focusedIndex: number | null;\n  onSelect: (prompt: SuggestedPrompt) => void;\n  setFocusedIndex: (index: number | null) => void;\n}\n\nfunction PromptList({\n  prompts,\n  focusedIndex,\n  onSelect,\n  setFocusedIndex,\n}: PromptListProps) {\n  const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  const setItemRef = useCallback(\n    (index: number) => (node: HTMLButtonElement | null) => {\n      itemRefs.current[index] = node;\n    },\n    []\n  );\n\n  useEffect(() => {\n    if (focusedIndex !== null && itemRefs.current[focusedIndex]) {\n      itemRefs.current[focusedIndex]?.scrollIntoView({\n        block: \"nearest\",\n        behavior: \"smooth\",\n      });\n    }\n  }, [focusedIndex]);\n\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      const target = e.target as HTMLElement;\n      if (\n        target instanceof HTMLInputElement ||\n        target instanceof HTMLTextAreaElement ||\n        target instanceof HTMLButtonElement\n      ) {\n        if (target.type === \"button\" && target.closest('[role=\"tablist\"]')) {\n          return;\n        }\n        if (target.type === \"search\" || target.type === \"text\") {\n          return;\n        }\n      }\n\n      if (prompts.length === 0) return;\n\n      switch (e.key) {\n        case \"ArrowDown\": {\n          e.preventDefault();\n          const nextIndex =\n            focusedIndex === null\n              ? 0\n              : focusedIndex < prompts.length - 1\n                ? focusedIndex + 1\n                : 0;\n          setFocusedIndex(nextIndex);\n          itemRefs.current[nextIndex]?.focus();\n          break;\n        }\n        case \"ArrowUp\": {\n          e.preventDefault();\n          const prevIndex =\n            focusedIndex === null\n              ? prompts.length - 1\n              : focusedIndex > 0\n                ? focusedIndex - 1\n                : prompts.length - 1;\n          setFocusedIndex(prevIndex);\n          itemRefs.current[prevIndex]?.focus();\n          break;\n        }\n        case \"Home\": {\n          e.preventDefault();\n          setFocusedIndex(0);\n          itemRefs.current[0]?.focus();\n          break;\n        }\n        case \"End\": {\n          e.preventDefault();\n          const lastIndex = prompts.length - 1;\n          setFocusedIndex(lastIndex);\n          itemRefs.current[lastIndex]?.focus();\n          break;\n        }\n        case \"Escape\": {\n          if (focusedIndex !== null) {\n            e.preventDefault();\n            setFocusedIndex(null);\n            containerRef.current?.focus();\n          }\n          break;\n        }\n        case \"Enter\": {\n          if (focusedIndex !== null && itemRefs.current[focusedIndex]) {\n            e.preventDefault();\n            onSelect(prompts[focusedIndex]);\n          }\n          break;\n        }\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [prompts, focusedIndex, setFocusedIndex, onSelect]);\n\n  return (\n    <div\n      aria-label=\"Suggested prompts list\"\n      className=\"grid gap-3\"\n      ref={containerRef}\n      role=\"list\"\n      tabIndex={-1}\n    >\n      {prompts.map((prompt, index) => (\n        <PromptItem\n          index={index}\n          isFocused={focusedIndex === index}\n          itemRef={setItemRef(index)}\n          key={prompt.id}\n          onSelect={onSelect}\n          prompt={prompt}\n        />\n      ))}\n    </div>\n  );\n}\n\nexport default function AISuggestedPrompts({\n  prompts = [],\n  categories = defaultCategories,\n  onSelect,\n  onSearch,\n  className,\n  showSearch = true,\n  showCategories = true,\n  maxDisplay,\n  isLoading = false,\n}: AISuggestedPromptsProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [selectedCategory, setSelectedCategory] = useState<string>(\"All\");\n  const [focusedIndex, setFocusedIndex] = useState<number | null>(null);\n\n  const filteredPrompts = useMemo(() => {\n    let filtered = prompts;\n\n    if (selectedCategory !== \"All\") {\n      filtered = filtered.filter((p) => p.category === selectedCategory);\n    }\n\n    if (searchQuery.trim()) {\n      const query = searchQuery.toLowerCase();\n      filtered = filtered.filter(\n        (p) =>\n          p.title.toLowerCase().includes(query) ||\n          p.prompt.toLowerCase().includes(query) ||\n          p.description?.toLowerCase().includes(query)\n      );\n    }\n\n    const sorted = [...filtered].sort((a, b) => {\n      if (a.isPopular && !b.isPopular) return -1;\n      if (!a.isPopular && b.isPopular) return 1;\n      if (a.isRecent && !b.isRecent) return -1;\n      if (!a.isRecent && b.isRecent) return 1;\n      return 0;\n    });\n\n    return maxDisplay ? sorted.slice(0, maxDisplay) : sorted;\n  }, [prompts, selectedCategory, searchQuery, maxDisplay]);\n\n  const handleSelect = useCallback(\n    (prompt: SuggestedPrompt) => {\n      onSelect?.(prompt);\n      setFocusedIndex(null);\n    },\n    [onSelect]\n  );\n\n  const handleSearchChange = useCallback(\n    (value: string) => {\n      setSearchQuery(value);\n      onSearch?.(value);\n      setFocusedIndex(null);\n    },\n    [onSearch]\n  );\n\n  const handleCategoryChange = useCallback((category: string) => {\n    setSelectedCategory(category);\n    setFocusedIndex(null);\n  }, []);\n\n  if (prompts.length === 0 && !isLoading) {\n    return (\n      <Card className={cn(\"w-full shadow-xs\", className)}>\n        <CardHeader>\n          <ComponentHeader\n            description=\"Get started with these helpful prompts\"\n            title=\"Suggested Prompts\"\n          />\n        </CardHeader>\n        <CardContent>\n          <EmptyState type=\"no-prompts\" />\n        </CardContent>\n      </Card>\n    );\n  }\n\n  return (\n    <Card className={cn(\"w-full shadow-xs\", className)}>\n      <CardHeader>\n        <div className=\"flex flex-col gap-4\">\n          <ComponentHeader\n            description=\"Get started with these helpful prompts\"\n            title=\"Suggested Prompts\"\n          />\n          {showSearch && (\n            <PromptSearch\n              onChange={handleSearchChange}\n              placeholder=\"Search prompts…\"\n              resultCount={filteredPrompts.length}\n              value={searchQuery}\n            />\n          )}\n          {showCategories && (\n            <CategoryFilter\n              categories={categories}\n              onSelect={handleCategoryChange}\n              selectedCategory={selectedCategory}\n            />\n          )}\n        </div>\n      </CardHeader>\n      <CardContent>\n        {isLoading ? (\n          <div className=\"flex flex-col items-center justify-center gap-4 py-12 text-center\">\n            <div className=\"flex size-12 items-center justify-center rounded-full bg-muted\">\n              <Lightbulb\n                aria-hidden=\"true\"\n                className=\"size-6 animate-pulse text-muted-foreground\"\n              />\n            </div>\n            <p className=\"text-muted-foreground text-sm\">Loading prompts…</p>\n          </div>\n        ) : filteredPrompts.length === 0 ? (\n          <EmptyState searchQuery={searchQuery} type=\"no-results\" />\n        ) : (\n          <PromptList\n            focusedIndex={focusedIndex}\n            onSelect={handleSelect}\n            prompts={filteredPrompts}\n            setFocusedIndex={setFocusedIndex}\n          />\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "ai"
  ],
  "type": "registry:ui"
}