{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-activity-feed",
  "title": "Team Activity Feed",
  "description": "Display team activity and events in a feed with filtering.",
  "registryDependencies": [
    "avatar",
    "badge",
    "button",
    "card",
    "empty",
    "input-group",
    "select",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/team/team-activity-feed.tsx",
      "content": "\"use client\";\n\nimport {\n  Activity,\n  Bot,\n  FileText,\n  MessageSquare,\n  Search,\n  UserPlus,\n  Users,\n} from \"lucide-react\";\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  Empty,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/registry/new-york/ui/empty\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/new-york/ui/select\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\n\nexport type ActivityType =\n  | \"member_joined\"\n  | \"member_left\"\n  | \"member_role_changed\"\n  | \"ai_session_created\"\n  | \"file_uploaded\"\n  | \"file_deleted\"\n  | \"note_created\"\n  | \"note_updated\"\n  | \"project_created\"\n  | \"project_updated\"\n  | \"chat_message\"\n  | \"settings_updated\";\n\nexport interface ActivityEntry {\n  id: string;\n  type: ActivityType;\n  user: {\n    id: string;\n    name: string;\n    avatar?: string;\n  };\n  description: string;\n  metadata?: Record<string, unknown>;\n  timestamp: Date;\n  projectId?: string;\n  projectName?: string;\n}\n\nexport interface TeamActivityFeedProps {\n  activities?: ActivityEntry[];\n  onFilterChange?: (filters: {\n    type?: ActivityType;\n    userId?: string;\n    projectId?: string;\n  }) => void;\n  className?: string;\n  showFilters?: boolean;\n  showSearch?: boolean;\n  itemsPerPage?: number;\n}\n\nfunction formatTime(date: Date): string {\n  return new Intl.DateTimeFormat(\"en-US\", {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n  }).format(date);\n}\n\nfunction formatDate(date: Date): string {\n  const today = new Date();\n  const yesterday = new Date(today);\n  yesterday.setDate(yesterday.getDate() - 1);\n\n  if (date.toDateString() === today.toDateString()) {\n    return \"Today\";\n  }\n  if (date.toDateString() === yesterday.toDateString()) {\n    return \"Yesterday\";\n  }\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 getActivityIcon(type: ActivityType) {\n  switch (type) {\n    case \"member_joined\":\n    case \"member_left\":\n      return UserPlus;\n    case \"member_role_changed\":\n      return Users;\n    case \"ai_session_created\":\n      return Bot;\n    case \"file_uploaded\":\n    case \"file_deleted\":\n      return FileText;\n    case \"note_created\":\n    case \"note_updated\":\n      return MessageSquare;\n    case \"chat_message\":\n      return MessageSquare;\n    default:\n      return Activity;\n  }\n}\n\nfunction getActivityColor(type: ActivityType): string {\n  switch (type) {\n    case \"member_joined\":\n      return \"text-green-600\";\n    case \"member_left\":\n      return \"text-red-600\";\n    case \"ai_session_created\":\n      return \"text-primary\";\n    default:\n      return \"text-muted-foreground\";\n  }\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 TeamActivityFeed({\n  activities = [],\n  onFilterChange,\n  className,\n  showFilters = true,\n  showSearch = true,\n  itemsPerPage = 20,\n}: TeamActivityFeedProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [typeFilter, setTypeFilter] = useState<string>(\"all\");\n  const [displayedCount, setDisplayedCount] = useState(itemsPerPage);\n\n  const filteredActivities = useMemo(() => {\n    let filtered = activities;\n\n    if (typeFilter !== \"all\") {\n      filtered = filtered.filter((a) => a.type === typeFilter);\n    }\n\n    if (searchQuery.trim()) {\n      const query = searchQuery.toLowerCase();\n      filtered = filtered.filter(\n        (a) =>\n          a.description.toLowerCase().includes(query) ||\n          a.user.name.toLowerCase().includes(query) ||\n          a.projectName?.toLowerCase().includes(query)\n      );\n    }\n\n    return filtered;\n  }, [activities, typeFilter, searchQuery]);\n\n  const displayedActivities = filteredActivities.slice(0, displayedCount);\n  const hasMore = displayedActivities.length < filteredActivities.length;\n\n  const groupedActivities = displayedActivities.reduce(\n    (groups, activity) => {\n      const dateKey = formatDate(activity.timestamp);\n      if (!groups[dateKey]) {\n        groups[dateKey] = [];\n      }\n      groups[dateKey].push(activity);\n      return groups;\n    },\n    {} as Record<string, ActivityEntry[]>\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 gap-1\">\n            <CardTitle>Activity Feed</CardTitle>\n            <CardDescription>Recent team activity and updates</CardDescription>\n          </div>\n          <div className=\"flex flex-col flex-wrap gap-2 md:flex-row\">\n            {showSearch && (\n              <InputGroup className=\"flex-1\">\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 activities…\"\n                  type=\"search\"\n                  value={searchQuery}\n                />\n              </InputGroup>\n            )}\n            {showFilters && (\n              <Select onValueChange={setTypeFilter} value={typeFilter}>\n                <SelectTrigger className=\"w-full md:w-[180px]\">\n                  <SelectValue placeholder=\"All types\" />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"all\">All types</SelectItem>\n                  <SelectItem value=\"member_joined\">Member joined</SelectItem>\n                  <SelectItem value=\"ai_session_created\">\n                    AI sessions\n                  </SelectItem>\n                  <SelectItem value=\"file_uploaded\">File uploads</SelectItem>\n                  <SelectItem value=\"note_created\">Notes</SelectItem>\n                  <SelectItem value=\"project_created\">Projects</SelectItem>\n                  <SelectItem value=\"chat_message\">Chat messages</SelectItem>\n                </SelectContent>\n              </Select>\n            )}\n          </div>\n        </div>\n      </CardHeader>\n      <CardContent>\n        {displayedActivities.length === 0 ? (\n          <Empty>\n            <EmptyHeader>\n              <EmptyMedia variant=\"icon\">\n                <Activity className=\"size-6\" />\n              </EmptyMedia>\n              <EmptyTitle>\n                {searchQuery || typeFilter !== \"all\"\n                  ? \"No activities found\"\n                  : \"No activities yet\"}\n              </EmptyTitle>\n            </EmptyHeader>\n          </Empty>\n        ) : (\n          <div className=\"flex flex-col gap-6\">\n            {Object.entries(groupedActivities).map(\n              ([dateKey, dateActivities]) => (\n                <div key={dateKey}>\n                  <div className=\"relative mb-8\">\n                    <Separator />\n                    <div className=\"absolute inset-0 flex items-center justify-center\">\n                      <span className=\"bg-background px-2 text-muted-foreground text-xs\">\n                        {dateKey}\n                      </span>\n                    </div>\n                  </div>\n                  <div className=\"flex flex-col gap-4\">\n                    {dateActivities.map((activity) => {\n                      const Icon = getActivityIcon(activity.type);\n                      return (\n                        <div\n                          className=\"flex items-start gap-3\"\n                          key={activity.id}\n                        >\n                          <Avatar className=\"size-8 shrink-0\">\n                            <AvatarImage\n                              alt={activity.user.name}\n                              src={activity.user.avatar}\n                            />\n                            <AvatarFallback className=\"text-xs\">\n                              {getInitials(activity.user.name)}\n                            </AvatarFallback>\n                          </Avatar>\n                          <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n                            <div className=\"flex items-center gap-2\">\n                              <span className=\"font-medium text-sm\">\n                                {activity.user.name}\n                              </span>\n                              <div\n                                className={cn(\n                                  \"flex size-4 items-center justify-center rounded-full\",\n                                  getActivityColor(activity.type)\n                                )}\n                              >\n                                <Icon className=\"size-3\" />\n                              </div>\n                              {activity.projectName && (\n                                <Badge className=\"text-xs\" variant=\"outline\">\n                                  {activity.projectName}\n                                </Badge>\n                              )}\n                            </div>\n                            <p className=\"wrap-break-word text-sm\">\n                              {activity.description}\n                            </p>\n                            <span className=\"text-muted-foreground text-xs\">\n                              {formatRelativeTime(activity.timestamp)}\n                            </span>\n                          </div>\n                        </div>\n                      );\n                    })}\n                  </div>\n                </div>\n              )\n            )}\n            {hasMore && (\n              <Button\n                className=\"w-full\"\n                onClick={() => setDisplayedCount((prev) => prev + itemsPerPage)}\n                type=\"button\"\n                variant=\"outline\"\n              >\n                Load More\n              </Button>\n            )}\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "team"
  ],
  "type": "registry:ui"
}