{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-projects",
  "title": "Team Projects",
  "description": "Manage team projects and tasks with member assignment.",
  "registryDependencies": [
    "avatar",
    "badge",
    "button",
    "card",
    "dialog",
    "dropdown-menu",
    "empty",
    "field",
    "input-group",
    "textarea"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/team/team-projects.tsx",
      "content": "\"use client\";\n\nimport {\n  Folder,\n  FolderOpen,\n  Loader2,\n  MoreVertical,\n  Plus,\n  Search,\n  Settings,\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 TeamProject {\n  id: string;\n  name: string;\n  description?: string;\n  color?: string;\n  icon?: string;\n  members: {\n    id: string;\n    name: string;\n    avatar?: string;\n  }[];\n  defaultModel?: string;\n  aiUsage?: {\n    tokens: number;\n    sessions: number;\n  };\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nexport interface TeamProjectsProps {\n  projects?: TeamProject[];\n  currentUserId?: string;\n  onCreate?: (data: {\n    name: string;\n    description?: string;\n    color?: string;\n    defaultModel?: string;\n  }) => Promise<TeamProject>;\n  onUpdate?: (projectId: string, data: Partial<TeamProject>) => Promise<void>;\n  onDelete?: (projectId: string) => Promise<void>;\n  onSelect?: (projectId: string) => void;\n  className?: string;\n  showSearch?: 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 formatNumber(num: number): string {\n  if (num >= 1_000_000) {\n    return `${(num / 1_000_000).toFixed(1)}M`;\n  }\n  if (num >= 1000) {\n    return `${(num / 1000).toFixed(1)}K`;\n  }\n  return num.toString();\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 TeamProjects({\n  projects = [],\n  currentUserId,\n  onCreate,\n  onUpdate,\n  onDelete,\n  onSelect,\n  className,\n  showSearch = true,\n}: TeamProjectsProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [createDialogOpen, setCreateDialogOpen] = useState(false);\n  const [isCreating, setIsCreating] = useState(false);\n  const [actionLoading, setActionLoading] = useState<string | null>(null);\n  const [projectData, setProjectData] = useState({\n    name: \"\",\n    description: \"\",\n    color: \"#3b82f6\",\n    defaultModel: \"\",\n  });\n\n  const filteredProjects = projects.filter((project) => {\n    if (!searchQuery.trim()) return true;\n    const query = searchQuery.toLowerCase();\n    return (\n      project.name.toLowerCase().includes(query) ||\n      project.description?.toLowerCase().includes(query)\n    );\n  });\n\n  const handleCreate = async () => {\n    if (!(projectData.name.trim() && onCreate)) return;\n\n    setIsCreating(true);\n    try {\n      await onCreate(projectData);\n      setProjectData({\n        name: \"\",\n        description: \"\",\n        color: \"#3b82f6\",\n        defaultModel: \"\",\n      });\n      setCreateDialogOpen(false);\n    } finally {\n      setIsCreating(false);\n    }\n  };\n\n  const handleAction = async (\n    action: () => Promise<void>,\n    projectId: string\n  ) => {\n    setActionLoading(projectId);\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>Projects</CardTitle>\n              <CardDescription>\n                {projects.length} project{projects.length !== 1 ? \"s\" : \"\"} in\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 Project\n                  </Button>\n                </DialogTrigger>\n                <DialogContent className=\"md:max-w-md\">\n                  <DialogHeader>\n                    <DialogTitle>Create Project</DialogTitle>\n                    <DialogDescription>\n                      Create a new team project workspace\n                    </DialogDescription>\n                  </DialogHeader>\n                  <div className=\"flex flex-col gap-4\">\n                    <Field>\n                      <FieldLabel htmlFor=\"project-name\">Name</FieldLabel>\n                      <FieldContent>\n                        <InputGroup>\n                          <InputGroupInput\n                            id=\"project-name\"\n                            onChange={(\n                              e: React.ChangeEvent<HTMLInputElement>\n                            ) =>\n                              setProjectData((prev) => ({\n                                ...prev,\n                                name: e.target.value,\n                              }))\n                            }\n                            placeholder=\"Project name…\"\n                            type=\"text\"\n                            value={projectData.name}\n                          />\n                        </InputGroup>\n                      </FieldContent>\n                    </Field>\n                    <Field>\n                      <FieldLabel htmlFor=\"project-description\">\n                        Description\n                      </FieldLabel>\n                      <FieldContent>\n                        <Textarea\n                          id=\"project-description\"\n                          onChange={(\n                            e: React.ChangeEvent<HTMLTextAreaElement>\n                          ) =>\n                            setProjectData((prev) => ({\n                              ...prev,\n                              description: e.target.value,\n                            }))\n                          }\n                          placeholder=\"What is this project about?\"\n                          rows={3}\n                          value={projectData.description}\n                        />\n                      </FieldContent>\n                    </Field>\n                    <Field>\n                      <FieldLabel htmlFor=\"project-color\">Color</FieldLabel>\n                      <FieldContent>\n                        <div className=\"flex items-center gap-3\">\n                          <input\n                            className=\"size-10 rounded border\"\n                            id=\"project-color\"\n                            onChange={(\n                              e: React.ChangeEvent<HTMLInputElement>\n                            ) =>\n                              setProjectData((prev) => ({\n                                ...prev,\n                                color: e.target.value,\n                              }))\n                            }\n                            type=\"color\"\n                            value={projectData.color}\n                          />\n                          <InputGroup className=\"flex-1\">\n                            <InputGroupInput\n                              onChange={(\n                                e: React.ChangeEvent<HTMLInputElement>\n                              ) =>\n                                setProjectData((prev) => ({\n                                  ...prev,\n                                  color: e.target.value,\n                                }))\n                              }\n                              placeholder=\"#3b82f6\"\n                              type=\"text\"\n                              value={projectData.color}\n                            />\n                          </InputGroup>\n                        </div>\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={!projectData.name.trim()}\n                      onClick={handleCreate}\n                      type=\"button\"\n                    >\n                      {isCreating ? (\n                        <>\n                          <Loader2 className=\"size-4 animate-spin\" />\n                          Creating…\n                        </>\n                      ) : (\n                        \"Create Project\"\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 projects…\"\n                type=\"search\"\n                value={searchQuery}\n              />\n            </InputGroup>\n          )}\n        </div>\n      </CardHeader>\n      <CardContent>\n        {filteredProjects.length === 0 ? (\n          <Empty>\n            <EmptyHeader>\n              <EmptyMedia variant=\"icon\">\n                <Folder className=\"size-6\" />\n              </EmptyMedia>\n              <EmptyTitle>\n                {searchQuery ? \"No projects found\" : \"No projects yet\"}\n              </EmptyTitle>\n            </EmptyHeader>\n          </Empty>\n        ) : (\n          <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n            {filteredProjects.map((project) => (\n              <div\n                className=\"group flex flex-col justify-between gap-3 rounded-lg border bg-card p-4 transition-colors hover:border-primary hover:shadow-sm\"\n                key={project.id}\n              >\n                <div className=\"flex items-start justify-between gap-2\">\n                  <div className=\"flex items-center gap-2\">\n                    <div\n                      className=\"flex size-10 items-center justify-center rounded-lg\"\n                      style={{ backgroundColor: `${project.color}20` }}\n                    >\n                      {project.icon ? (\n                        <span className=\"text-lg\">{project.icon}</span>\n                      ) : (\n                        <Folder\n                          className=\"size-5\"\n                          style={{ color: project.color }}\n                        />\n                      )}\n                    </div>\n                    <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n                      <h3 className=\"wrap-break-word font-semibold text-base leading-tight\">\n                        {project.name}\n                      </h3>\n                      {project.defaultModel && (\n                        <Badge className=\"w-fit text-xs\" variant=\"outline\">\n                          {project.defaultModel}\n                        </Badge>\n                      )}\n                    </div>\n                  </div>\n                  <DropdownMenu>\n                    <DropdownMenuTrigger asChild>\n                      <Button\n                        aria-label={`More options for ${project.name}`}\n                        size=\"icon-sm\"\n                        type=\"button\"\n                        variant=\"ghost\"\n                      >\n                        {actionLoading === project.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                      {onSelect && (\n                        <DropdownMenuItem onSelect={() => onSelect(project.id)}>\n                          <FolderOpen className=\"size-4\" />\n                          Open\n                        </DropdownMenuItem>\n                      )}\n                      {onUpdate && (\n                        <>\n                          <DropdownMenuSeparator />\n                          <DropdownMenuItem\n                            onSelect={() => onUpdate(project.id, {})}\n                          >\n                            <Settings className=\"size-4\" />\n                            Settings\n                          </DropdownMenuItem>\n                        </>\n                      )}\n                      {onDelete && (\n                        <>\n                          <DropdownMenuSeparator />\n                          <DropdownMenuItem\n                            onSelect={() =>\n                              handleAction(\n                                () => onDelete(project.id),\n                                project.id\n                              )\n                            }\n                            variant=\"destructive\"\n                          >\n                            <Trash2 className=\"size-4\" />\n                            Delete\n                          </DropdownMenuItem>\n                        </>\n                      )}\n                    </DropdownMenuContent>\n                  </DropdownMenu>\n                </div>\n                {project.description && (\n                  <p className=\"wrap-break-word line-clamp-2 text-muted-foreground text-sm\">\n                    {project.description}\n                  </p>\n                )}\n                {project.aiUsage && (\n                  <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n                    <span>{formatNumber(project.aiUsage.tokens)} tokens</span>\n                    <span aria-hidden=\"true\">•</span>\n                    <span>\n                      {project.aiUsage.sessions} session\n                      {project.aiUsage.sessions !== 1 ? \"s\" : \"\"}\n                    </span>\n                  </div>\n                )}\n                <div className=\"flex items-center justify-between\">\n                  <div className=\"flex -space-x-2\">\n                    {project.members.slice(0, 3).map((member) => (\n                      <Avatar\n                        className=\"size-6 border-2 border-background\"\n                        key={member.id}\n                      >\n                        <AvatarImage alt={member.name} src={member.avatar} />\n                        <AvatarFallback className=\"text-xs\">\n                          {getInitials(member.name)}\n                        </AvatarFallback>\n                      </Avatar>\n                    ))}\n                    {project.members.length > 3 && (\n                      <div className=\"flex size-6 items-center justify-center rounded-full border-2 border-background bg-muted\">\n                        <span className=\"text-muted-foreground text-xs\">\n                          +{project.members.length - 3}\n                        </span>\n                      </div>\n                    )}\n                  </div>\n                  <span className=\"text-muted-foreground text-xs\">\n                    Updated {formatDate(project.updatedAt)}\n                  </span>\n                </div>\n              </div>\n            ))}\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "team"
  ],
  "type": "registry:ui"
}