{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-notes",
  "title": "Team Notes",
  "description": "Collaborative notes and documentation with AI summarization.",
  "registryDependencies": [
    "avatar",
    "badge",
    "button",
    "card",
    "dialog",
    "dropdown-menu",
    "empty",
    "field",
    "input-group",
    "textarea"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/team/team-notes.tsx",
      "content": "\"use client\";\n\nimport {\n  Edit,\n  Loader2,\n  MoreVertical,\n  Plus,\n  Search,\n  Sparkles,\n  Trash2,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@/registry/new-york/ui/avatar\";\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  DialogTrigger,\n} from \"@/registry/new-york/ui/dialog\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/registry/new-york/ui/dropdown-menu\";\nimport {\n  Empty,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/registry/new-york/ui/empty\";\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 { Textarea } from \"@/registry/new-york/ui/textarea\";\n\nexport interface TeamNote {\n  id: string;\n  title: string;\n  content: string;\n  author: {\n    id: string;\n    name: string;\n    avatar?: string;\n  };\n  tags?: string[];\n  aiSummary?: string;\n  createdAt: Date;\n  updatedAt: Date;\n  lastEditedBy?: {\n    id: string;\n    name: string;\n  };\n  participants?: {\n    id: string;\n    name: string;\n    avatar?: string;\n  }[];\n}\n\nexport interface TeamNotesProps {\n  notes?: TeamNote[];\n  currentUserId?: string;\n  onCreate?: (data: {\n    title: string;\n    content: string;\n    tags?: string[];\n  }) => Promise<TeamNote>;\n  onUpdate?: (\n    noteId: string,\n    data: { title?: string; content?: string; tags?: string[] }\n  ) => Promise<void>;\n  onDelete?: (noteId: string) => Promise<void>;\n  onSummarize?: (noteId: string) => Promise<string>;\n  className?: string;\n  showSearch?: boolean;\n  showTags?: boolean;\n}\n\nfunction formatDate(date: Date): string {\n  return new Intl.DateTimeFormat(\"en-US\", {\n    month: \"short\",\n    day: \"numeric\",\n    year: \"numeric\",\n  }).format(date);\n}\n\nfunction formatRelativeTime(date: Date): string {\n  const now = Date.now();\n  const diff = now - date.getTime();\n  const minutes = Math.floor(diff / 60_000);\n  const hours = Math.floor(minutes / 60);\n  const days = Math.floor(hours / 24);\n\n  if (minutes < 1) return \"Just now\";\n  if (minutes < 60) return `${minutes}m ago`;\n  if (hours < 24) return `${hours}h ago`;\n  if (days < 7) return `${days}d ago`;\n  return formatDate(date);\n}\n\nfunction getInitials(name: string): string {\n  return name\n    .split(\" \")\n    .map((n) => n[0])\n    .join(\"\")\n    .toUpperCase()\n    .slice(0, 2);\n}\n\nexport default function TeamNotes({\n  notes = [],\n  currentUserId,\n  onCreate,\n  onUpdate,\n  onDelete,\n  onSummarize,\n  className,\n  showSearch = true,\n  showTags = true,\n}: TeamNotesProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [createDialogOpen, setCreateDialogOpen] = useState(false);\n  const [editingNote, setEditingNote] = useState<string | null>(null);\n  const [isCreating, setIsCreating] = useState(false);\n  const [isSummarizing, setIsSummarizing] = useState<string | null>(null);\n  const [actionLoading, setActionLoading] = useState<string | null>(null);\n  const [noteData, setNoteData] = useState({\n    title: \"\",\n    content: \"\",\n    tags: [] as string[],\n  });\n\n  const filteredNotes = notes.filter((note) => {\n    if (!searchQuery.trim()) return true;\n    const query = searchQuery.toLowerCase();\n    return (\n      note.title.toLowerCase().includes(query) ||\n      note.content.toLowerCase().includes(query) ||\n      note.tags?.some((tag) => tag.toLowerCase().includes(query))\n    );\n  });\n\n  const handleCreate = async () => {\n    if (!(noteData.title.trim() && onCreate)) return;\n\n    setIsCreating(true);\n    try {\n      await onCreate(noteData);\n      setNoteData({ title: \"\", content: \"\", tags: [] });\n      setCreateDialogOpen(false);\n    } finally {\n      setIsCreating(false);\n    }\n  };\n\n  const handleSummarize = async (noteId: string) => {\n    if (!onSummarize) return;\n    setIsSummarizing(noteId);\n    try {\n      await onSummarize(noteId);\n    } finally {\n      setIsSummarizing(null);\n    }\n  };\n\n  const handleAction = async (action: () => Promise<void>, noteId: string) => {\n    setActionLoading(noteId);\n    try {\n      await action();\n    } finally {\n      setActionLoading(null);\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          <div className=\"flex flex-col flex-wrap gap-3 md:flex-row md:items-start md:justify-between\">\n            <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n              <CardTitle>Team Notes</CardTitle>\n              <CardDescription>\n                {notes.length} note{notes.length !== 1 ? \"s\" : \"\"} shared with\n                your team\n              </CardDescription>\n            </div>\n            {onCreate && (\n              <Dialog\n                onOpenChange={setCreateDialogOpen}\n                open={createDialogOpen}\n              >\n                <DialogTrigger asChild>\n                  <Button className=\"w-full shrink-0 md:w-auto\" type=\"button\">\n                    <Plus className=\"size-4\" />\n                    New Note\n                  </Button>\n                </DialogTrigger>\n                <DialogContent className=\"md:max-w-2xl\">\n                  <DialogHeader>\n                    <DialogTitle>Create Note</DialogTitle>\n                    <DialogDescription>\n                      Create a new collaborative note\n                    </DialogDescription>\n                  </DialogHeader>\n                  <div className=\"flex flex-col gap-4\">\n                    <Field>\n                      <FieldLabel htmlFor=\"note-title\">Title</FieldLabel>\n                      <FieldContent>\n                        <InputGroup>\n                          <InputGroupInput\n                            id=\"note-title\"\n                            onChange={(\n                              e: React.ChangeEvent<HTMLInputElement>\n                            ) =>\n                              setNoteData((prev) => ({\n                                ...prev,\n                                title: e.target.value,\n                              }))\n                            }\n                            placeholder=\"Note title…\"\n                            type=\"text\"\n                            value={noteData.title}\n                          />\n                        </InputGroup>\n                      </FieldContent>\n                    </Field>\n                    <Field>\n                      <FieldLabel htmlFor=\"note-content\">Content</FieldLabel>\n                      <FieldContent>\n                        <Textarea\n                          id=\"note-content\"\n                          onChange={(\n                            e: React.ChangeEvent<HTMLTextAreaElement>\n                          ) =>\n                            setNoteData((prev) => ({\n                              ...prev,\n                              content: e.target.value,\n                            }))\n                          }\n                          placeholder=\"Write your note…\"\n                          rows={8}\n                          value={noteData.content}\n                        />\n                      </FieldContent>\n                    </Field>\n                  </div>\n                  <DialogFooter>\n                    <Button\n                      onClick={() => setCreateDialogOpen(false)}\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      Cancel\n                    </Button>\n                    <Button\n                      aria-busy={isCreating}\n                      data-loading={isCreating}\n                      disabled={!noteData.title.trim()}\n                      onClick={handleCreate}\n                      type=\"button\"\n                    >\n                      {isCreating ? (\n                        <>\n                          <Loader2 className=\"size-4 animate-spin\" />\n                          Creating…\n                        </>\n                      ) : (\n                        \"Create Note\"\n                      )}\n                    </Button>\n                  </DialogFooter>\n                </DialogContent>\n              </Dialog>\n            )}\n          </div>\n          {showSearch && (\n            <InputGroup>\n              <InputGroupAddon>\n                <Search className=\"size-4\" />\n              </InputGroupAddon>\n              <InputGroupInput\n                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>\n                  setSearchQuery(e.target.value)\n                }\n                placeholder=\"Search notes…\"\n                type=\"search\"\n                value={searchQuery}\n              />\n            </InputGroup>\n          )}\n        </div>\n      </CardHeader>\n      <CardContent>\n        {filteredNotes.length === 0 ? (\n          <Empty>\n            <EmptyHeader>\n              <EmptyMedia variant=\"icon\">\n                <Edit className=\"size-6\" />\n              </EmptyMedia>\n              <EmptyTitle>\n                {searchQuery ? \"No notes found\" : \"No notes yet\"}\n              </EmptyTitle>\n            </EmptyHeader>\n          </Empty>\n        ) : (\n          <div className=\"grid gap-4 md:grid-cols-2\">\n            {filteredNotes.map((note) => (\n              <div\n                className=\"group flex flex-col gap-3 rounded-lg border bg-card p-4 transition-colors hover:border-primary hover:shadow-sm\"\n                key={note.id}\n              >\n                <div className=\"flex items-start justify-between gap-2\">\n                  <h3 className=\"wrap-break-word font-semibold text-base leading-tight\">\n                    {note.title}\n                  </h3>\n                  <DropdownMenu>\n                    <DropdownMenuTrigger asChild>\n                      <Button\n                        aria-label={`More options for ${note.title}`}\n                        size=\"icon-sm\"\n                        type=\"button\"\n                        variant=\"ghost\"\n                      >\n                        {actionLoading === note.id ? (\n                          <Loader2 className=\"size-4 animate-spin\" />\n                        ) : (\n                          <MoreVertical className=\"size-4\" />\n                        )}\n                      </Button>\n                    </DropdownMenuTrigger>\n                    <DropdownMenuContent\n                      align=\"end\"\n                      collisionPadding={8}\n                      sideOffset={4}\n                    >\n                      {onSummarize && (\n                        <DropdownMenuItem\n                          disabled={isSummarizing === note.id}\n                          onSelect={() => handleSummarize(note.id)}\n                        >\n                          {isSummarizing === note.id ? (\n                            <>\n                              <Loader2 className=\"size-4 animate-spin\" />\n                              Summarizing…\n                            </>\n                          ) : (\n                            <>\n                              <Sparkles className=\"size-4\" />\n                              Summarize with AI\n                            </>\n                          )}\n                        </DropdownMenuItem>\n                      )}\n                      {onUpdate && (\n                        <>\n                          <DropdownMenuSeparator />\n                          <DropdownMenuItem\n                            onSelect={() => setEditingNote(note.id)}\n                          >\n                            <Edit className=\"size-4\" />\n                            Edit\n                          </DropdownMenuItem>\n                        </>\n                      )}\n                      {onDelete && (\n                        <>\n                          <DropdownMenuSeparator />\n                          <DropdownMenuItem\n                            onSelect={() =>\n                              handleAction(() => onDelete(note.id), note.id)\n                            }\n                            variant=\"destructive\"\n                          >\n                            <Trash2 className=\"size-4\" />\n                            Delete\n                          </DropdownMenuItem>\n                        </>\n                      )}\n                    </DropdownMenuContent>\n                  </DropdownMenu>\n                </div>\n                <p className=\"wrap-break-word line-clamp-3 text-muted-foreground text-sm\">\n                  {note.content}\n                </p>\n                {note.aiSummary && (\n                  <div className=\"rounded-lg border bg-muted/30 p-3\">\n                    <div className=\"mb-2 flex items-center gap-2\">\n                      <Sparkles className=\"size-3 text-primary\" />\n                      <span className=\"font-medium text-xs\">AI Summary</span>\n                    </div>\n                    <p className=\"text-muted-foreground text-xs\">\n                      {note.aiSummary}\n                    </p>\n                  </div>\n                )}\n                {showTags && note.tags && note.tags.length > 0 && (\n                  <div className=\"flex flex-wrap gap-1\">\n                    {note.tags.map((tag) => (\n                      <Badge className=\"text-xs\" key={tag} variant=\"outline\">\n                        {tag}\n                      </Badge>\n                    ))}\n                  </div>\n                )}\n                <div className=\"flex items-center justify-between\">\n                  <div className=\"flex items-center gap-2\">\n                    <Avatar className=\"size-6\">\n                      <AvatarImage\n                        alt={note.author.name}\n                        src={note.author.avatar}\n                      />\n                      <AvatarFallback className=\"text-xs\">\n                        {getInitials(note.author.name)}\n                      </AvatarFallback>\n                    </Avatar>\n                    <span className=\"text-muted-foreground text-xs\">\n                      {note.author.name}\n                    </span>\n                  </div>\n                  <span className=\"text-muted-foreground text-xs\">\n                    {formatRelativeTime(note.updatedAt)}\n                  </span>\n                </div>\n                {note.participants && note.participants.length > 0 && (\n                  <div className=\"flex items-center gap-2\">\n                    <div className=\"flex -space-x-2\">\n                      {note.participants.slice(0, 3).map((participant) => (\n                        <Avatar\n                          className=\"size-6 border-2 border-background\"\n                          key={participant.id}\n                        >\n                          <AvatarImage\n                            alt={participant.name}\n                            src={participant.avatar}\n                          />\n                          <AvatarFallback className=\"text-xs\">\n                            {getInitials(participant.name)}\n                          </AvatarFallback>\n                        </Avatar>\n                      ))}\n                    </div>\n                    {note.participants.length > 3 && (\n                      <span className=\"text-muted-foreground text-xs\">\n                        +{note.participants.length - 3} more\n                      </span>\n                    )}\n                  </div>\n                )}\n              </div>\n            ))}\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "team"
  ],
  "type": "registry:ui"
}