{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "project-list",
  "title": "Project List",
  "description": "Display and manage projects with progress tracking, member avatars, and task counts.",
  "registryDependencies": [
    "avatar",
    "badge",
    "button",
    "card",
    "dropdown-menu",
    "input-group",
    "progress",
    "select"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/tasks/project-list.tsx",
      "content": "\"use client\";\n\nimport {\n  Archive,\n  Calendar,\n  MoreVertical,\n  Plus,\n  Search,\n  Trash2,\n  User,\n  X,\n} from \"lucide-react\";\nimport Image from \"next/image\";\nimport { useMemo, 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  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/registry/new-york/ui/dropdown-menu\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport { Progress } from \"@/registry/new-york/ui/progress\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/new-york/ui/select\";\n\nexport type ProjectStatus = \"active\" | \"archived\" | \"completed\";\n\nexport interface ProjectMember {\n  id: string;\n  name: string;\n  avatar?: string;\n}\n\nexport interface Project {\n  id: string;\n  name: string;\n  description?: string;\n  status: ProjectStatus;\n  progress: number;\n  thumbnail?: string;\n  color?: string;\n  members: ProjectMember[];\n  taskCount?: number;\n  completedTaskCount?: number;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nexport interface ProjectListProps {\n  projects?: Project[];\n  onProjectSelect?: (projectId: string) => void;\n  onProjectCreate?: () => void;\n  onProjectArchive?: (projectId: string) => Promise<void>;\n  onProjectDelete?: (projectId: string) => Promise<void>;\n  className?: string;\n  showSearch?: boolean;\n  showFilters?: boolean;\n  layout?: \"grid\" | \"list\";\n}\n\nfunction formatDate(date: Date): string {\n  const now = new Date();\n  const month = date.toLocaleString(\"en-US\", { month: \"short\" });\n  const day = date.getDate();\n  const year = date.getFullYear();\n  const currentYear = now.getFullYear();\n\n  if (year === currentYear) {\n    return `${month} ${day}`;\n  }\n  return `${month} ${day}, ${year}`;\n}\n\nfunction getStatusBadgeVariant(status: ProjectStatus) {\n  switch (status) {\n    case \"active\":\n      return \"default\";\n    case \"completed\":\n      return \"secondary\";\n    case \"archived\":\n      return \"outline\";\n  }\n}\n\nfunction getStatusLabel(status: ProjectStatus): string {\n  switch (status) {\n    case \"active\":\n      return \"Active\";\n    case \"completed\":\n      return \"Completed\";\n    case \"archived\":\n      return \"Archived\";\n  }\n}\n\ninterface ProjectCardProps {\n  project: Project;\n  layout: \"grid\" | \"list\";\n  onProjectClick: (projectId: string) => void;\n  onArchive: (projectId: string) => void;\n  onDelete: (projectId: string) => void;\n}\n\nfunction ProjectCard({\n  project,\n  layout,\n  onProjectClick,\n  onArchive,\n  onDelete,\n}: ProjectCardProps) {\n  if (layout === \"list\") {\n    return (\n      <div\n        className=\"group flex items-center gap-4 rounded-lg border bg-card p-4 transition-colors hover:bg-accent\"\n        onClick={() => onProjectClick(project.id)}\n        role=\"button\"\n        tabIndex={0}\n      >\n        {project.thumbnail ? (\n          <Image\n            alt={project.name}\n            className=\"size-12 rounded-lg object-cover\"\n            height={48}\n            src={project.thumbnail}\n            width={48}\n          />\n        ) : (\n          <div\n            className=\"flex size-12 items-center justify-center rounded-lg font-semibold text-lg text-white\"\n            style={{\n              backgroundColor: project.color || \"hsl(var(--primary))\",\n            }}\n          >\n            {project.name.charAt(0).toUpperCase()}\n          </div>\n        )}\n        <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n          <div className=\"flex items-center gap-2\">\n            <h3 className=\"wrap-break-word font-semibold text-base\">\n              {project.name}\n            </h3>\n            <Badge variant={getStatusBadgeVariant(project.status)}>\n              {getStatusLabel(project.status)}\n            </Badge>\n          </div>\n          {project.description && (\n            <p className=\"line-clamp-1 text-muted-foreground text-sm\">\n              {project.description}\n            </p>\n          )}\n          <div className=\"flex items-center gap-4\">\n            <div className=\"flex items-center gap-1\">\n              <User className=\"size-3 text-muted-foreground\" />\n              <span className=\"text-muted-foreground text-xs\">\n                {project.members.length} member\n                {project.members.length !== 1 ? \"s\" : \"\"}\n              </span>\n            </div>\n            {project.taskCount !== undefined && (\n              <span className=\"text-muted-foreground text-xs\">\n                {project.completedTaskCount || 0} / {project.taskCount} tasks\n              </span>\n            )}\n            <span className=\"text-muted-foreground text-xs\">\n              Updated {formatDate(project.updatedAt)}\n            </span>\n          </div>\n          <Progress className=\"h-1.5\" value={project.progress} />\n        </div>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button\n              aria-label={`More options for ${project.name}`}\n              className=\"opacity-0 transition-opacity group-hover:opacity-100\"\n              onClick={(e) => e.stopPropagation()}\n              size=\"icon\"\n              type=\"button\"\n              variant=\"ghost\"\n            >\n              <MoreVertical className=\"size-4\" />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuItem onClick={() => onArchive(project.id)}>\n              <Archive className=\"size-4\" />\n              Archive\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem\n              onClick={() => onDelete(project.id)}\n              variant=\"destructive\"\n            >\n              <Trash2 className=\"size-4\" />\n              Delete\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className=\"group flex flex-col gap-3 rounded-lg border bg-card p-4 transition-all hover:shadow-md\"\n      onClick={() => onProjectClick(project.id)}\n      role=\"button\"\n      tabIndex={0}\n    >\n      {project.thumbnail ? (\n        <Image\n          alt={project.name}\n          className=\"h-32 w-full rounded-lg object-cover\"\n          height={128}\n          src={project.thumbnail}\n          width={256}\n        />\n      ) : (\n        <div\n          className=\"flex h-32 items-center justify-center rounded-lg font-semibold text-2xl text-white\"\n          style={{\n            backgroundColor: project.color || \"hsl(var(--primary))\",\n          }}\n        >\n          {project.name.charAt(0).toUpperCase()}\n        </div>\n      )}\n      <div className=\"flex flex-col gap-2\">\n        <div className=\"flex items-center justify-between\">\n          <h3 className=\"wrap-break-word font-semibold text-base\">\n            {project.name}\n          </h3>\n          <Badge variant={getStatusBadgeVariant(project.status)}>\n            {getStatusLabel(project.status)}\n          </Badge>\n        </div>\n        {project.description && (\n          <p className=\"line-clamp-2 text-muted-foreground text-sm\">\n            {project.description}\n          </p>\n        )}\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex -space-x-2\">\n            {project.members.slice(0, 4).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                  {member.name.charAt(0).toUpperCase()}\n                </AvatarFallback>\n              </Avatar>\n            ))}\n            {project.members.length > 4 && (\n              <div className=\"flex size-6 items-center justify-center rounded-full border-2 border-background bg-muted text-muted-foreground text-xs\">\n                +{project.members.length - 4}\n              </div>\n            )}\n          </div>\n          {project.taskCount !== undefined && (\n            <span className=\"text-muted-foreground text-xs\">\n              {project.completedTaskCount || 0} / {project.taskCount}\n            </span>\n          )}\n        </div>\n        <Progress className=\"h-1.5\" value={project.progress} />\n        <div className=\"flex items-center justify-between\">\n          <span className=\"text-muted-foreground text-xs\">\n            {project.progress}% complete\n          </span>\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button\n                aria-label={`More options for ${project.name}`}\n                className=\"opacity-0 transition-opacity group-hover:opacity-100\"\n                onClick={(e) => e.stopPropagation()}\n                size=\"icon\"\n                type=\"button\"\n                variant=\"ghost\"\n              >\n                <MoreVertical className=\"size-4\" />\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\">\n              <DropdownMenuItem onClick={() => onArchive(project.id)}>\n                <Archive className=\"size-4\" />\n                Archive\n              </DropdownMenuItem>\n              <DropdownMenuSeparator />\n              <DropdownMenuItem\n                onClick={() => onDelete(project.id)}\n                variant=\"destructive\"\n              >\n                <Trash2 className=\"size-4\" />\n                Delete\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default function ProjectList({\n  projects = [],\n  onProjectSelect,\n  onProjectCreate,\n  onProjectArchive,\n  onProjectDelete,\n  className,\n  showSearch = true,\n  showFilters = true,\n  layout = \"grid\",\n}: ProjectListProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [statusFilter, setStatusFilter] = useState<string>(\"all\");\n\n  const filteredProjects = useMemo(() => {\n    let filtered = [...projects];\n\n    if (searchQuery.trim()) {\n      const query = searchQuery.toLowerCase();\n      filtered = filtered.filter(\n        (project) =>\n          project.name.toLowerCase().includes(query) ||\n          project.description?.toLowerCase().includes(query)\n      );\n    }\n\n    if (statusFilter !== \"all\") {\n      filtered = filtered.filter((project) => project.status === statusFilter);\n    }\n\n    return filtered.sort(\n      (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()\n    );\n  }, [projects, searchQuery, statusFilter]);\n\n  const handleArchive = async (projectId: string) => {\n    await onProjectArchive?.(projectId);\n  };\n\n  const handleDelete = async (projectId: string) => {\n    await onProjectDelete?.(projectId);\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-wrap items-center justify-between gap-3\">\n            <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n              <CardTitle>Projects</CardTitle>\n              <CardDescription>\n                {filteredProjects.length} project\n                {filteredProjects.length !== 1 ? \"s\" : \"\"}\n              </CardDescription>\n            </div>\n            {onProjectCreate && (\n              <Button onClick={onProjectCreate} type=\"button\">\n                <Plus className=\"size-4\" />\n                New Project\n              </Button>\n            )}\n          </div>\n          {showSearch && (\n            <InputGroup>\n              <InputGroupAddon>\n                <Search className=\"size-4\" />\n              </InputGroupAddon>\n              <InputGroupInput\n                onChange={(e) => setSearchQuery(e.target.value)}\n                placeholder=\"Search projects…\"\n                type=\"search\"\n                value={searchQuery}\n              />\n              {searchQuery && (\n                <Button\n                  aria-label=\"Clear search\"\n                  className=\"absolute top-1/2 right-2 -translate-y-1/2\"\n                  onClick={() => setSearchQuery(\"\")}\n                  size=\"icon\"\n                  type=\"button\"\n                  variant=\"ghost\"\n                >\n                  <X className=\"size-4\" />\n                </Button>\n              )}\n            </InputGroup>\n          )}\n          {showFilters && (\n            <Select onValueChange={setStatusFilter} value={statusFilter}>\n              <SelectTrigger className=\"w-full sm:w-[180px]\">\n                <SelectValue placeholder=\"Filter by status\" />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"all\">All Status</SelectItem>\n                <SelectItem value=\"active\">Active</SelectItem>\n                <SelectItem value=\"completed\">Completed</SelectItem>\n                <SelectItem value=\"archived\">Archived</SelectItem>\n              </SelectContent>\n            </Select>\n          )}\n        </div>\n      </CardHeader>\n      <CardContent>\n        {filteredProjects.length === 0 ? (\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              <Calendar className=\"size-6 text-muted-foreground\" />\n            </div>\n            <div className=\"flex flex-col gap-2\">\n              <p className=\"font-medium text-sm\">\n                {searchQuery || statusFilter !== \"all\"\n                  ? \"No projects match your filters\"\n                  : \"No projects yet\"}\n              </p>\n              <p className=\"text-muted-foreground text-sm\">\n                {onProjectCreate\n                  ? \"Create your first project to get started\"\n                  : \"Projects will appear here\"}\n              </p>\n            </div>\n            {onProjectCreate && (\n              <Button onClick={onProjectCreate} type=\"button\" variant=\"outline\">\n                <Plus className=\"size-4\" />\n                Create Project\n              </Button>\n            )}\n          </div>\n        ) : (\n          <div\n            className={cn(\n              \"flex gap-4\",\n              layout === \"grid\"\n                ? \"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\"\n                : \"flex-col\"\n            )}\n          >\n            {filteredProjects.map((project) => (\n              <ProjectCard\n                key={project.id}\n                layout={layout}\n                onArchive={handleArchive}\n                onDelete={handleDelete}\n                onProjectClick={onProjectSelect || (() => {})}\n                project={project}\n              />\n            ))}\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "tasks"
  ],
  "type": "registry:ui"
}