{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-prompt-templates",
  "title": "AI Prompt Templates",
  "description": "Browse and use pre-built prompt templates with variables.",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "dialog",
    "field",
    "input-group",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/ai/ai-prompt-templates.tsx",
      "content": "\"use client\";\n\nimport {\n  BookOpen,\n  Eye,\n  Heart,\n  Loader2,\n  Plus,\n  Search,\n  Sparkles,\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  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/registry/new-york/ui/dialog\";\nimport { Field, FieldContent, FieldLabel } from \"@/registry/new-york/ui/field\";\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 PromptTemplate {\n  id: string;\n  name: string;\n  description: string;\n  category: string;\n  prompt: string;\n  variables?: Array<{\n    name: string;\n    label: string;\n    placeholder?: string;\n    required?: boolean;\n  }>;\n  isFavorite?: boolean;\n  isPopular?: boolean;\n  usageCount?: number;\n  author?: string;\n  tags?: string[];\n  icon?: React.ComponentType<{ className?: string }>;\n}\n\nexport interface AIPromptTemplatesProps {\n  templates?: PromptTemplate[];\n  categories?: string[];\n  onSelect?: (\n    template: PromptTemplate,\n    filledVariables?: Record<string, string>\n  ) => void;\n  onFavorite?: (templateId: string, isFavorite: boolean) => Promise<void>;\n  onCreate?: () => void;\n  className?: string;\n  showSearch?: boolean;\n  showCategories?: boolean;\n  showFavorites?: boolean;\n}\n\nconst defaultCategories = [\n  \"All\",\n  \"Writing\",\n  \"Code\",\n  \"Analysis\",\n  \"Creative\",\n  \"Research\",\n  \"Business\",\n];\n\nconst categoryIcons: Record<\n  string,\n  React.ComponentType<{ className?: string }>\n> = {\n  Writing: BookOpen,\n  Code: Sparkles,\n  Analysis: Sparkles,\n  Creative: Sparkles,\n  Research: BookOpen,\n  Business: Sparkles,\n};\n\nfunction fillTemplatePrompt(\n  prompt: string,\n  variables: Record<string, string>\n): string {\n  let filled = prompt;\n  Object.entries(variables).forEach(([key, value]) => {\n    filled = filled.replace(new RegExp(`\\\\{${key}\\\\}`, \"g\"), value);\n  });\n  return filled;\n}\n\ninterface TemplateHeaderProps {\n  onCreate?: () => void;\n}\n\nfunction TemplateHeader({ onCreate }: TemplateHeaderProps) {\n  return (\n    <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n      <div className=\"flex flex-col gap-1\">\n        <CardTitle>Prompt Templates</CardTitle>\n        <CardDescription>\n          Browse and use pre-built prompt templates\n        </CardDescription>\n      </div>\n      {onCreate && (\n        <Button\n          className=\"min-h-[44px] w-full min-w-[32px] sm:min-h-[32px] sm:w-auto\"\n          onClick={onCreate}\n          type=\"button\"\n        >\n          <Plus className=\"size-4\" />\n          Create Template\n        </Button>\n      )}\n    </div>\n  );\n}\n\ninterface TemplateSearchProps {\n  value: string;\n  onChange: (value: string) => void;\n  resultCount?: number;\n}\n\nfunction TemplateSearch({ value, onChange, resultCount }: TemplateSearchProps) {\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 handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Escape\") {\n        onChange(\"\");\n        inputRef.current?.blur();\n      }\n    },\n    [onChange]\n  );\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 templates\"\n          onChange={(e) => onChange(e.target.value)}\n          onKeyDown={handleKeyDown}\n          placeholder=\"Search templates…\"\n          ref={inputRef}\n          type=\"search\"\n          value={value}\n        />\n      </InputGroup>\n      {value && resultCount !== undefined && (\n        <p className=\"mt-2 text-muted-foreground text-xs\" role=\"status\">\n          {resultCount} {resultCount === 1 ? \"result\" : \"results\"} found\n        </p>\n      )}\n    </div>\n  );\n}\n\ninterface CategoryFilterProps {\n  categories: string[];\n  selectedCategory: string;\n  onCategoryChange: (category: string) => void;\n  showFavorites: boolean;\n  showFavoritesOnly: boolean;\n  onToggleFavorites: () => void;\n}\n\nfunction CategoryFilter({\n  categories,\n  selectedCategory,\n  onCategoryChange,\n  showFavorites,\n  showFavoritesOnly,\n  onToggleFavorites,\n}: CategoryFilterProps) {\n  return (\n    <div\n      aria-label=\"Filter templates by category\"\n      className=\"flex flex-wrap items-center gap-2\"\n      role=\"tablist\"\n    >\n      {showFavorites && (\n        <Button\n          aria-label={\n            showFavoritesOnly ? \"Show all templates\" : \"Show favorites only\"\n          }\n          className={cn(\n            \"min-h-[32px] gap-2 px-3 py-1.5 sm:h-auto sm:min-h-[10px]\",\n            showFavoritesOnly\n              ? \"border border-border bg-accent text-accent-foreground\"\n              : \"bg-muted text-muted-foreground hover:bg-muted/80\"\n          )}\n          onClick={onToggleFavorites}\n          type=\"button\"\n          variant=\"ghost\"\n        >\n          <Heart aria-hidden=\"true\" className=\"size-3.5\" />\n          <span className=\"text-xs\">Favorites</span>\n        </Button>\n      )}\n      {categories.map((category) => {\n        const Icon = categoryIcons[category] || Sparkles;\n        const isSelected = selectedCategory === category;\n        return (\n          <Button\n            aria-label={`Filter by ${category} category`}\n            aria-pressed={undefined}\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={() => onCategoryChange(category)}\n            role={undefined}\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            {category !== \"All\" && (\n              <Icon aria-hidden=\"true\" className=\"size-3.5\" />\n            )}\n            <span className=\"text-xs\">{category}</span>\n          </Button>\n        );\n      })}\n    </div>\n  );\n}\n\ninterface EmptyStateProps {\n  message: string;\n  icon?: React.ComponentType<{ className?: string }>;\n  action?: React.ReactNode;\n}\n\nfunction EmptyState({\n  message,\n  icon: Icon = BookOpen,\n  action,\n}: EmptyStateProps) {\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        <Icon aria-hidden=\"true\" className=\"size-6 text-muted-foreground\" />\n      </div>\n      <p className=\"text-muted-foreground text-sm\">{message}</p>\n      {action}\n    </div>\n  );\n}\n\ninterface TemplateCardProps {\n  template: PromptTemplate;\n  onSelect: (template: PromptTemplate) => void;\n  onFavorite?: (templateId: string, isFavorite: boolean) => Promise<void>;\n  isFavoriting: boolean;\n  optimisticFavorite?: boolean;\n  index: number;\n  isFocused: boolean;\n  onFocus: () => void;\n}\n\nfunction TemplateCard({\n  template,\n  onSelect,\n  onFavorite,\n  isFavoriting,\n  optimisticFavorite,\n  index,\n  isFocused,\n  onFocus,\n}: TemplateCardProps) {\n  const cardRef = useRef<HTMLDivElement>(null);\n  const Icon = template.icon || categoryIcons[template.category] || Sparkles;\n  const hasVariables = template.variables && template.variables.length > 0;\n  const isFavorite = optimisticFavorite ?? template.isFavorite ?? false;\n\n  useEffect(() => {\n    if (isFocused && cardRef.current) {\n      cardRef.current.scrollIntoView({ block: \"nearest\", behavior: \"smooth\" });\n    }\n  }, [isFocused]);\n\n  const handleFavorite = useCallback(\n    async (e: React.MouseEvent) => {\n      e.stopPropagation();\n      if (!onFavorite) return;\n      await onFavorite(template.id, !isFavorite);\n    },\n    [onFavorite, template.id, isFavorite]\n  );\n\n  const handleSelect = useCallback(() => {\n    onSelect(template);\n  }, [onSelect, template]);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Enter\" || e.key === \" \") {\n        e.preventDefault();\n        handleSelect();\n      }\n    },\n    [handleSelect]\n  );\n\n  return (\n    <article\n      aria-label={`Template: ${template.name}`}\n      className={cn(\n        \"group flex flex-col gap-4 rounded-lg border bg-card p-5 transition-all focus-within:border-border focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 hover:border-border hover:shadow-sm\",\n        isFocused && \"border-border ring-2 ring-ring ring-offset-2\"\n      )}\n      onKeyDown={handleKeyDown}\n      ref={cardRef}\n      role=\"listitem\"\n      tabIndex={0}\n    >\n      <div className=\"flex items-center gap-3\">\n        <div className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground transition-colors group-hover:bg-muted/80\">\n          <Icon aria-hidden=\"true\" className=\"size-5\" />\n        </div>\n        <div className=\"flex min-w-0 flex-1 flex-col justify-center\">\n          <div className=\"flex flex-wrap items-center justify-between gap-2\">\n            <h3 className=\"wrap-break-word font-semibold text-base leading-tight\">\n              {template.name}\n            </h3>\n            <div className=\"flex shrink-0 items-center gap-1.5\">\n              {template.isPopular && (\n                <Badge\n                  aria-label=\"Popular template\"\n                  className=\"text-xs\"\n                  variant=\"secondary\"\n                >\n                  Popular\n                </Badge>\n              )}\n              {onFavorite && (\n                <button\n                  aria-label={`${isFavorite ? \"Remove from\" : \"Add to\"} favorites`}\n                  className=\"min-h-[44px] min-w-[32px] rounded-md p-1.5 transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:min-h-[32px]\"\n                  disabled={isFavoriting}\n                  onClick={handleFavorite}\n                  type=\"button\"\n                >\n                  {isFavoriting ? (\n                    <Loader2\n                      aria-hidden=\"true\"\n                      className=\"size-4 animate-spin text-muted-foreground\"\n                    />\n                  ) : (\n                    <Heart\n                      aria-hidden=\"true\"\n                      className={cn(\n                        \"size-4 transition-colors\",\n                        isFavorite\n                          ? \"fill-red-500 text-red-500\"\n                          : \"text-muted-foreground group-hover:text-foreground\"\n                      )}\n                    />\n                  )}\n                </button>\n              )}\n            </div>\n          </div>\n          <p className=\"wrap-break-word line-clamp-2 text-muted-foreground text-sm\">\n            {template.description}\n          </p>\n        </div>\n      </div>\n\n      <div className=\"flex flex-wrap items-center gap-2\">\n        {template.tags && template.tags.length > 0 && (\n          <>\n            {template.tags.slice(0, 2).map((tag) => (\n              <Badge className=\"text-xs\" key={tag} variant=\"outline\">\n                {tag}\n              </Badge>\n            ))}\n            {template.tags.length > 2 && (\n              <span\n                aria-label={`${template.tags.length - 2} more tags`}\n                className=\"text-muted-foreground text-xs\"\n              >\n                +{template.tags.length - 2}\n              </span>\n            )}\n          </>\n        )}\n        {template.usageCount !== undefined && (\n          <span className=\"font-tabular-nums text-muted-foreground text-xs\">\n            {template.usageCount} {template.usageCount === 1 ? \"use\" : \"uses\"}\n          </span>\n        )}\n        {hasVariables && (\n          <Badge className=\"text-xs\" variant=\"secondary\">\n            {template.variables!.length} variable\n            {template.variables!.length !== 1 ? \"s\" : \"\"}\n          </Badge>\n        )}\n      </div>\n\n      <div className=\"flex items-center gap-2\">\n        <Button\n          aria-label={`Use template: ${template.name}`}\n          className=\"min-h-[44px] flex-1 sm:min-h-[32px]\"\n          onClick={handleSelect}\n          type=\"button\"\n          variant=\"outline\"\n        >\n          {hasVariables ? \"Configure & Use\" : \"Use Template\"}\n        </Button>\n      </div>\n    </article>\n  );\n}\n\ninterface TemplateDialogProps {\n  template: PromptTemplate | null;\n  variables: Record<string, string>;\n  onVariablesChange: (variables: Record<string, string>) => void;\n  onClose: () => void;\n  onUse: () => void;\n  canUse: boolean;\n}\n\nfunction TemplateDialog({\n  template,\n  variables,\n  onVariablesChange,\n  onClose,\n  onUse,\n  canUse,\n}: TemplateDialogProps) {\n  const [showPreview, setShowPreview] = useState(false);\n  const firstInputRef = useRef<HTMLInputElement>(null);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        onClose();\n      }\n    },\n    [onClose]\n  );\n\n  useEffect(() => {\n    if (template && template.variables && template.variables.length > 0) {\n      setTimeout(() => {\n        firstInputRef.current?.focus();\n      }, 100);\n    }\n  }, [template]);\n\n  if (!template) return null;\n\n  const Icon = template.icon || categoryIcons[template.category] || Sparkles;\n  const previewPrompt = fillTemplatePrompt(template.prompt, variables);\n\n  return (\n    <Dialog onOpenChange={(open) => !open && onClose()} open={!!template}>\n      <DialogContent\n        className=\"flex flex-col gap-4 p-4 sm:max-w-2xl sm:gap-6 sm:p-6\"\n        onKeyDown={handleKeyDown}\n      >\n        <DialogHeader className=\"shrink-0\">\n          <div className=\"flex items-start gap-3\">\n            <div className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground\">\n              <Icon aria-hidden=\"true\" className=\"size-5\" />\n            </div>\n            <div className=\"flex min-w-0 flex-1 flex-col gap-2 text-left\">\n              <DialogTitle className=\"wrap-break-word\">\n                {template.name}\n              </DialogTitle>\n              <DialogDescription className=\"wrap-break-word\">\n                {template.description}\n              </DialogDescription>\n            </div>\n          </div>\n        </DialogHeader>\n\n        <div className=\"flex flex-1 flex-col gap-6 overflow-y-auto\">\n          {template.variables && template.variables.length > 0 ? (\n            <div className=\"flex flex-col gap-4\">\n              <h3 className=\"font-medium text-sm\">Fill in the details</h3>\n              <div className=\"flex flex-col gap-4\">\n                {template.variables.map((variable, index) => (\n                  <Field key={variable.name}>\n                    <FieldLabel htmlFor={`${template.id}-${variable.name}`}>\n                      {variable.label}\n                      {variable.required && (\n                        <span\n                          aria-label=\"required\"\n                          className=\"text-destructive\"\n                        >\n                          {\" \"}\n                          *\n                        </span>\n                      )}\n                    </FieldLabel>\n                    <FieldContent>\n                      <InputGroup>\n                        <InputGroupInput\n                          id={`${template.id}-${variable.name}`}\n                          onChange={(e) => {\n                            onVariablesChange({\n                              ...variables,\n                              [variable.name]: e.target.value,\n                            });\n                          }}\n                          placeholder={\n                            variable.placeholder ||\n                            `Enter ${variable.label.toLowerCase()}…`\n                          }\n                          ref={index === 0 ? firstInputRef : undefined}\n                          type=\"text\"\n                          value={variables[variable.name] || \"\"}\n                        />\n                      </InputGroup>\n                    </FieldContent>\n                  </Field>\n                ))}\n              </div>\n            </div>\n          ) : null}\n\n          <Separator />\n\n          <div className=\"flex flex-col gap-3\">\n            <div className=\"flex items-center justify-between\">\n              <h3 className=\"font-medium text-sm\">Preview</h3>\n              <Button\n                aria-expanded={showPreview}\n                aria-label={showPreview ? \"Hide preview\" : \"Show preview\"}\n                className=\"h-auto min-h-[44px] gap-2 px-2 py-1 sm:min-h-[32px]\"\n                onClick={() => setShowPreview(!showPreview)}\n                type=\"button\"\n                variant=\"ghost\"\n              >\n                <Eye aria-hidden=\"true\" className=\"size-4\" />\n                <span className=\"text-xs\">\n                  {showPreview ? \"Hide\" : \"Show\"} Preview\n                </span>\n              </Button>\n            </div>\n            {showPreview && (\n              <div className=\"rounded-lg border bg-muted/30 p-4\">\n                <p className=\"wrap-break-word whitespace-pre-wrap text-sm\">\n                  {previewPrompt || template.prompt}\n                </p>\n              </div>\n            )}\n          </div>\n        </div>\n\n        <DialogFooter className=\"shrink-0\">\n          <Button\n            className=\"min-h-[44px] sm:min-h-[32px]\"\n            onClick={onClose}\n            type=\"button\"\n            variant=\"outline\"\n          >\n            Cancel\n          </Button>\n          <Button\n            className=\"min-h-[44px] sm:min-h-[32px]\"\n            disabled={!canUse}\n            onClick={onUse}\n            type=\"button\"\n          >\n            Use Template\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default function AIPromptTemplates({\n  templates = [],\n  categories = defaultCategories,\n  onSelect,\n  onFavorite,\n  onCreate,\n  className,\n  showSearch = true,\n  showCategories = true,\n  showFavorites = true,\n}: AIPromptTemplatesProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [selectedCategory, setSelectedCategory] = useState<string>(\"All\");\n  const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);\n  const [selectedTemplate, setSelectedTemplate] =\n    useState<PromptTemplate | null>(null);\n  const [templateVariables, setTemplateVariables] = useState<\n    Record<string, string>\n  >({});\n  const [isFavoriting, setIsFavoriting] = useState<string | null>(null);\n  const [focusedIndex, setFocusedIndex] = useState<number>(-1);\n  const [optimisticFavorites, setOptimisticFavorites] = useState<\n    Record<string, boolean>\n  >({});\n\n  const filteredTemplates = useMemo(() => {\n    let filtered = templates;\n\n    if (showFavoritesOnly) {\n      filtered = filtered.filter((t) => {\n        const isFavorite = optimisticFavorites[t.id] ?? t.isFavorite ?? false;\n        return isFavorite;\n      });\n    }\n\n    if (selectedCategory !== \"All\") {\n      filtered = filtered.filter((t) => t.category === selectedCategory);\n    }\n\n    if (searchQuery.trim()) {\n      const query = searchQuery.toLowerCase();\n      filtered = filtered.filter(\n        (t) =>\n          t.name.toLowerCase().includes(query) ||\n          t.description.toLowerCase().includes(query) ||\n          t.prompt.toLowerCase().includes(query) ||\n          t.tags?.some((tag) => tag.toLowerCase().includes(query))\n      );\n    }\n\n    return filtered.sort((a, b) => {\n      if (a.isPopular && !b.isPopular) return -1;\n      if (!a.isPopular && b.isPopular) return 1;\n      const aFavorite = optimisticFavorites[a.id] ?? a.isFavorite ?? false;\n      const bFavorite = optimisticFavorites[b.id] ?? b.isFavorite ?? false;\n      if (aFavorite && !bFavorite) return -1;\n      if (!aFavorite && bFavorite) return 1;\n      return (b.usageCount || 0) - (a.usageCount || 0);\n    });\n  }, [\n    templates,\n    selectedCategory,\n    searchQuery,\n    showFavoritesOnly,\n    optimisticFavorites,\n  ]);\n\n  useEffect(() => {\n    if (focusedIndex >= filteredTemplates.length) {\n      setFocusedIndex(-1);\n    }\n  }, [focusedIndex, filteredTemplates.length]);\n\n  const handleSelect = useCallback(\n    (template: PromptTemplate) => {\n      if (template.variables && template.variables.length > 0) {\n        setSelectedTemplate(template);\n        setTemplateVariables({});\n      } else {\n        onSelect?.(template);\n      }\n    },\n    [onSelect]\n  );\n\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (\n        e.target instanceof HTMLInputElement ||\n        e.target instanceof HTMLTextAreaElement\n      ) {\n        return;\n      }\n\n      if (e.key === \"ArrowDown\") {\n        e.preventDefault();\n        setFocusedIndex((prev) =>\n          prev < filteredTemplates.length - 1 ? prev + 1 : 0\n        );\n      } else if (e.key === \"ArrowUp\") {\n        e.preventDefault();\n        setFocusedIndex((prev) =>\n          prev > 0 ? prev - 1 : filteredTemplates.length - 1\n        );\n      } else if (e.key === \"Enter\" && focusedIndex >= 0) {\n        e.preventDefault();\n        const template = filteredTemplates[focusedIndex];\n        if (template) {\n          handleSelect(template);\n        }\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [filteredTemplates, focusedIndex, handleSelect]);\n\n  const handleUseTemplate = useCallback(() => {\n    if (!selectedTemplate) return;\n    onSelect?.(selectedTemplate, templateVariables);\n    setSelectedTemplate(null);\n    setTemplateVariables({});\n  }, [selectedTemplate, templateVariables, onSelect]);\n\n  const handleFavorite = useCallback(\n    async (templateId: string, isFavorite: boolean) => {\n      if (!onFavorite) return;\n\n      setOptimisticFavorites((prev) => ({\n        ...prev,\n        [templateId]: isFavorite,\n      }));\n\n      setIsFavoriting(templateId);\n      try {\n        await onFavorite(templateId, isFavorite);\n      } catch (error) {\n        setOptimisticFavorites((prev) => {\n          const next = { ...prev };\n          delete next[templateId];\n          return next;\n        });\n      } finally {\n        setIsFavoriting(null);\n      }\n    },\n    [onFavorite]\n  );\n\n  const canUseTemplate =\n    !selectedTemplate?.variables ||\n    selectedTemplate.variables.every(\n      (v) => !v.required || templateVariables[v.name]\n    );\n\n  if (templates.length === 0) {\n    return (\n      <Card className={cn(\"w-full shadow-xs\", className)}>\n        <CardHeader>\n          <TemplateHeader onCreate={onCreate} />\n        </CardHeader>\n        <CardContent>\n          <EmptyState\n            action={\n              onCreate ? (\n                <Button\n                  className=\"min-h-[44px] sm:min-h-[32px]\"\n                  onClick={onCreate}\n                  type=\"button\"\n                >\n                  <Plus className=\"size-4\" />\n                  Create Template\n                </Button>\n              ) : undefined\n            }\n            message=\"No templates available\"\n          />\n        </CardContent>\n      </Card>\n    );\n  }\n\n  return (\n    <>\n      <Card className={cn(\"w-full shadow-xs\", className)}>\n        <CardHeader>\n          <div className=\"flex flex-col gap-4\">\n            <TemplateHeader onCreate={onCreate} />\n            {showSearch && (\n              <TemplateSearch\n                onChange={setSearchQuery}\n                resultCount={filteredTemplates.length}\n                value={searchQuery}\n              />\n            )}\n            {showCategories && (\n              <CategoryFilter\n                categories={categories}\n                onCategoryChange={setSelectedCategory}\n                onToggleFavorites={() =>\n                  setShowFavoritesOnly(!showFavoritesOnly)\n                }\n                selectedCategory={selectedCategory}\n                showFavorites={showFavorites}\n                showFavoritesOnly={showFavoritesOnly}\n              />\n            )}\n          </div>\n        </CardHeader>\n        <CardContent>\n          {filteredTemplates.length === 0 ? (\n            <EmptyState\n              message={\n                searchQuery.trim()\n                  ? \"No templates match your search\"\n                  : showFavoritesOnly\n                    ? \"No favorite templates\"\n                    : \"No templates match your filters\"\n              }\n            />\n          ) : (\n            <ul\n              aria-label=\"Prompt templates\"\n              className=\"grid gap-4\"\n              role=\"list\"\n            >\n              {filteredTemplates.map((template, index) => (\n                <TemplateCard\n                  index={index}\n                  isFavoriting={isFavoriting === template.id}\n                  isFocused={focusedIndex === index}\n                  key={template.id}\n                  onFavorite={onFavorite ? handleFavorite : undefined}\n                  onFocus={() => setFocusedIndex(index)}\n                  onSelect={handleSelect}\n                  optimisticFavorite={optimisticFavorites[template.id]}\n                  template={template}\n                />\n              ))}\n            </ul>\n          )}\n        </CardContent>\n      </Card>\n\n      <TemplateDialog\n        canUse={canUseTemplate}\n        onClose={() => {\n          setSelectedTemplate(null);\n          setTemplateVariables({});\n        }}\n        onUse={handleUseTemplate}\n        onVariablesChange={setTemplateVariables}\n        template={selectedTemplate}\n        variables={templateVariables}\n      />\n    </>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "ai"
  ],
  "type": "registry:ui"
}