{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "activity-feed",
  "title": "Activity Feed",
  "description": "Activity Feed from the Get IT Done design system.",
  "dependencies": [
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/data-state-panel.json",
    "https://zero-design.itdone.de/r/heading-tag.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/activity-feed.tsx",
      "content": "'use client';\n\nimport { Bell, Clock } from 'lucide-react';\nimport { createContext, forwardRef, useContext, type HTMLAttributes, type LiHTMLAttributes, type ReactNode } from 'react';\n\nimport { focusRing, transitionBase } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport type { HeadingTag } from '@/components/ui/heading-tag';\nimport { DataStatePanel, SkeletonBar } from '@/components/ui/data-state-panel';\nimport { intlLocaleOf, useLabels, useLocale, usePreset } from '@/components/ui/labels-context';\n\n// A NotificationItem placed directly in a NotificationList (without a NotificationGroup, which owns the <ul>) renders a plain block, so it never yields a list item without a list. Anywhere else it stays an <li>.\nconst NotificationListContext = createContext<'list' | 'block'>('list');\n\ntype FeedState = 'default' | 'loading' | 'empty' | 'error';\n\ninterface FeedStateProps {\n  state?: FeedState;\n  emptyTitle?: string;\n  emptyDescription?: string;\n  emptyAction?: ReactNode;\n  errorTitle?: string;\n  errorDescription?: string;\n  onRetry?: () => void;\n  loadingLabel?: string;\n  loadingRows?: number;\n}\n\nconst FeedBody = ({\n  state,\n  emptyTitle,\n  emptyDescription,\n  emptyAction,\n  errorTitle,\n  errorDescription,\n  onRetry,\n  loadingLabel,\n  loadingRows = 4,\n  emptyIcon,\n  children,\n}: FeedStateProps & { emptyIcon: ReactNode; children: ReactNode; emptyTitle: string; errorTitle: string; loadingLabel: string }) => {\n  const l = useLabels();\n  if (state === 'loading') {\n    return (\n      <div role=\"status\" aria-label={loadingLabel} className=\"flex flex-col gap-4 p-3\">\n        {Array.from({ length: loadingRows }).map((_, i) => (\n          <div key={i} className=\"flex items-center gap-3\">\n            <SkeletonBar className=\"h-avatar-sm w-avatar-sm shrink-0 rounded-full\" />\n            <div className=\"flex flex-1 flex-col gap-2\">\n              <SkeletonBar className=\"w-2/3\" />\n              <SkeletonBar className=\"w-1/3\" />\n            </div>\n          </div>\n        ))}\n      </div>\n    );\n  }\n  if (state === 'empty') {\n    return <DataStatePanel kind=\"empty\" outlined icon={emptyIcon} title={emptyTitle} description={emptyDescription} action={emptyAction} />;\n  }\n  if (state === 'error') {\n    return <DataStatePanel kind=\"error\" title={errorTitle ?? l.couldNotLoad} description={errorDescription} onRetry={onRetry} />;\n  }\n  return <>{children}</>;\n};\n\n// Timeline\n\nconst timelineTone = {\n  neutral: 'bg-muted text-icon-subtle',\n  brand: 'bg-background-brand-subtle text-icon-brand',\n  success: 'bg-background-success-subtle text-icon-success',\n  destructive: 'bg-background-destructive-subtle text-icon-destructive',\n} as const;\n\n// Row density shared from Timeline to its items.\nconst TimelineDensityContext = createContext<'default' | 'compact'>('default');\n\nexport interface TimelineProps extends HTMLAttributes<HTMLOListElement>, FeedStateProps {\n  'aria-label': string;\n  // `compact` is the dense record history (Figma Activity feed Type=Timeline compact): tighter rows, semibold title, tinted icon circle.\n  density?: 'default' | 'compact';\n}\n\nconst Timeline = forwardRef<HTMLOListElement, TimelineProps>(\n  (\n    {\n      className,\n      density = 'default',\n      state = 'default',\n      emptyTitle: emptyTitleProp,\n      emptyDescription,\n      emptyAction,\n      errorTitle: errorTitleProp,\n      errorDescription: errorDescriptionProp,\n      onRetry,\n      loadingLabel: loadingLabelProp,\n      loadingRows,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const l = useLabels();\n    const emptyTitle = emptyTitleProp ?? l.timelineEmptyTitle;\n    const errorTitle = errorTitleProp ?? l.timelineErrorTitle;\n    const errorDescription = errorDescriptionProp ?? l.timelineErrorDescription;\n    const loadingLabel = loadingLabelProp ?? l.timelineLoading;\n    return (\n    <FeedBody\n      state={state}\n      emptyIcon={<Clock className=\"h-icon-md w-icon-md\" />}\n      emptyTitle={emptyTitle}\n      emptyDescription={emptyDescription}\n      emptyAction={emptyAction}\n      errorTitle={errorTitle}\n      errorDescription={errorDescription}\n      onRetry={onRetry}\n      loadingLabel={loadingLabel}\n      loadingRows={loadingRows}\n    >\n      <TimelineDensityContext.Provider value={density}>\n        <ol ref={ref} data-density={density} className={cn('m-0 flex list-none flex-col p-0', className)} {...props}>\n          {children}\n        </ol>\n      </TimelineDensityContext.Provider>\n    </FeedBody>\n  );\n  },\n);\nTimeline.displayName = 'Timeline';\n\nexport interface TimelineItemProps extends Omit<LiHTMLAttributes<HTMLLIElement>, 'title'> {\n  title: ReactNode;\n  // Supporting line, e.g. who did it.\n  meta?: ReactNode;\n  // Relative or absolute time, shown right aligned.\n  time?: ReactNode;\n  icon?: ReactNode;\n  // Avatar shown instead of the icon dot (use Avatar size xs so the connector stays centered). Tone is ignored.\n  avatar?: ReactNode;\n  tone?: keyof typeof timelineTone;\n}\n\nconst TimelineItem = forwardRef<HTMLLIElement, TimelineItemProps>(({ className, title, meta, time, icon, avatar, tone = 'neutral', ...props }, ref) => {\n  const compact = useContext(TimelineDensityContext) === 'compact';\n  // Zero: 28px marker with a 14px icon and a semibold title.\n  const zero = usePreset() === 'zero';\n  const marker = zero ? 'h-icon-tile w-icon-tile' : 'h-avatar-xs w-avatar-xs';\n  return (\n  <li\n    ref={ref}\n    className={cn(\n      'typography-ui relative flex items-start gap-3',\n      compact ? 'py-1' : 'py-2',\n      // connector line between dots\n      'before:absolute before:bottom-0 before:w-px before:bg-border-subtle last:before:hidden',\n      zero ? 'before:left-[calc(var(--size-icon-tile)/2)] before:top-10' : cn('before:left-3', compact ? 'before:top-8' : 'before:top-9'),\n      className,\n    )}\n    {...props}\n  >\n    {avatar ? (\n      <span className={cn('relative z-raised flex shrink-0 items-center justify-center', marker)}>{avatar}</span>\n    ) : (\n      <span aria-hidden=\"true\" className={cn('relative z-raised flex shrink-0 items-center justify-center rounded-full', marker, zero && '[&>svg]:h-icon-xs [&>svg]:w-icon-xs', timelineTone[tone])}>\n        {icon ?? <span className=\"h-2 w-2 rounded-full bg-current\" />}\n      </span>\n    )}\n    <div className=\"min-w-0 flex-1\">\n      <p className={cn('text-foreground', compact || zero ? 'typography-ui-semibold' : 'typography-ui-medium')}>{title}</p>\n      {meta ? <p className=\"typography-text-extra-small text-muted-foreground\">{meta}</p> : null}\n    </div>\n    {time ? <span className=\"typography-text-extra-small whitespace-nowrap text-muted-foreground\">{time}</span> : null}\n  </li>\n  );\n});\nTimelineItem.displayName = 'TimelineItem';\n\n// Milestone timeline\n\nexport interface Milestone {\n  id: string;\n  // Date of the milestone. A Date, an ISO string or a timestamp.\n  date: Date | string | number;\n  // Name of the milestone. Carried by the list, the axis itself is decorative.\n  label: ReactNode;\n}\n\ninterface MilestoneTimelineBaseProps extends Omit<HTMLAttributes<HTMLElement>, 'children'> {\n  milestones: Milestone[];\n  // Start and end of the axis. Default: first and last milestone.\n  start?: Date | string | number;\n  end?: Date | string | number;\n  // Cut-off date. The axis from this date on is emphasised and later dots use the brand colour.\n  cutoff?: Date | string | number;\n  // Edge labels under the axis. Default: the formatted start and end date.\n  startLabel?: ReactNode;\n  endLabel?: ReactNode;\n  caption?: ReactNode;\n}\n\n// Pass a Timeline as `children` to show it in a collapsible block, then `detailsLabel` (its summary) is required. Without children the milestones are listed for screen readers only.\nexport type MilestoneTimelineProps = MilestoneTimelineBaseProps &\n  ({ children?: undefined; detailsLabel?: undefined } | { children: ReactNode; detailsLabel: ReactNode });\n\nconst toTime = (value: Date | string | number) => (value instanceof Date ? value.getTime() : new Date(value).getTime());\n\n// Horizontal axis with dots at their proportional position in time (variant \"milestone\" of the timeline family). The axis is decorative, the list carries the information.\nconst MilestoneTimeline = forwardRef<HTMLElement, MilestoneTimelineProps>(\n  ({ className, milestones, start, end, cutoff, startLabel, endLabel, caption, children, detailsLabel, ...props }, ref) => {\n    const locale = intlLocaleOf(useLocale());\n    const times = milestones.map((m) => toTime(m.date));\n    const from = start !== undefined ? toTime(start) : Math.min(...times);\n    const to = end !== undefined ? toTime(end) : Math.max(...times);\n    const span = to - from;\n    const percent = (time: number) => (span > 0 ? Math.min(Math.max(((time - from) / span) * 100, 0), 100) : 0);\n    const format = (time: number) => new Intl.DateTimeFormat(locale, { day: 'numeric', month: 'short', year: 'numeric' }).format(time);\n    const cutoffTime = cutoff !== undefined ? toTime(cutoff) : undefined;\n    const cutoffPercent = cutoffTime !== undefined ? percent(cutoffTime) : undefined;\n    return (\n      <figure ref={ref} data-variant=\"milestone\" className={cn('m-0 flex flex-col gap-3', className)} {...props}>\n        <div aria-hidden=\"true\" className=\"flex flex-col gap-2\">\n          <div className=\"relative mx-1 h-4\">\n            <span className=\"absolute inset-x-0 top-1/2 h-px bg-border-default\" />\n            {cutoffPercent !== undefined ? (\n              <span data-axis-cutoff=\"\" className=\"absolute top-1/2 h-px bg-border-bold\" style={{ left: `${cutoffPercent}%`, right: 0 }} />\n            ) : null}\n            {milestones.map((m, i) => (\n              <span\n                key={m.id}\n                data-milestone-dot=\"\"\n                className={cn(\n                  'absolute top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full',\n                  cutoffTime !== undefined && times[i] >= cutoffTime ? 'bg-background-brand-bold-default' : 'bg-icon-subtle',\n                )}\n                style={{ left: `${percent(times[i])}%` }}\n              />\n            ))}\n          </div>\n          <div className=\"typography-code flex justify-between px-1 text-text-subtle\">\n            <span>{startLabel ?? format(from)}</span>\n            <span>{endLabel ?? format(to)}</span>\n          </div>\n        </div>\n        {caption ? <figcaption className=\"typography-text-small text-text-subtle\">{caption}</figcaption> : null}\n        {children ? (\n          <details className=\"group\">\n            <summary className={cn('typography-ui-medium cursor-pointer rounded-item text-text-default', focusRing)}>{detailsLabel}</summary>\n            <div className=\"pt-3\">{children}</div>\n          </details>\n        ) : (\n          <ol className=\"sr-only\">\n            {milestones.map((m, i) => (\n              <li key={m.id}>\n                {format(times[i])}: {m.label}\n              </li>\n            ))}\n          </ol>\n        )}\n      </figure>\n    );\n  },\n);\nMilestoneTimeline.displayName = 'MilestoneTimeline';\n\n// Notifications\n\nexport interface NotificationListProps extends Omit<HTMLAttributes<HTMLElement>, 'title'>, FeedStateProps {\n  title?: ReactNode;\n  // Heading element of the title, so the list fits the document outline. Default h2.\n  titleAs?: HeadingTag;\n  // Popover is a floating surface, inbox is flat.\n  variant?: 'popover' | 'inbox';\n  // Shows the \"Mark all read\" action when set.\n  onMarkAllRead?: () => void;\n  markAllReadLabel?: string;\n}\n\nconst NotificationList = forwardRef<HTMLElement, NotificationListProps>(\n  (\n    {\n      className,\n      title: titleProp,\n      titleAs: Title = 'h2',\n      variant = 'popover',\n      state = 'default',\n      onMarkAllRead,\n      markAllReadLabel: markAllReadLabelProp,\n      emptyTitle: emptyTitleProp,\n      emptyDescription: emptyDescriptionProp,\n      emptyAction,\n      errorTitle: errorTitleProp,\n      errorDescription: errorDescriptionProp,\n      onRetry,\n      loadingLabel: loadingLabelProp,\n      loadingRows = 3,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const l = useLabels();\n    const title = titleProp ?? l.notifications;\n    const markAllReadLabel = markAllReadLabelProp ?? l.markAllRead;\n    const emptyTitle = emptyTitleProp ?? l.notificationsEmptyTitle;\n    const emptyDescription = emptyDescriptionProp ?? l.notificationsEmptyDescription;\n    const errorTitle = errorTitleProp ?? l.notificationsErrorTitle;\n    const errorDescription = errorDescriptionProp ?? l.notificationsErrorDescription;\n    const loadingLabel = loadingLabelProp ?? l.notificationsLoading;\n    return (\n    <section\n      ref={ref}\n      aria-label={typeof title === 'string' ? title : l.notifications}\n      data-variant={variant}\n      className={cn(\n        'flex flex-col bg-popover text-popover-foreground',\n        variant === 'popover' ? 'rounded-surface border border-solid border-hairline shadow-popover' : 'bg-transparent',\n        className,\n      )}\n      {...props}\n    >\n      <header className=\"flex items-center justify-between gap-2 px-3 py-2\">\n        <Title className=\"typography-ui-semibold\">{title}</Title>\n        {onMarkAllRead && state === 'default' ? (\n          <button\n            type=\"button\"\n            onClick={onMarkAllRead}\n            className={cn('typography-text-extra-small h-6 rounded-item px-2 text-muted-foreground hover:text-foreground', transitionBase, focusRing)}\n          >\n            {markAllReadLabel}\n          </button>\n        ) : null}\n      </header>\n      <NotificationListContext.Provider value=\"block\">\n      <FeedBody\n        state={state}\n        emptyIcon={<Bell className=\"h-icon-md w-icon-md\" />}\n        emptyTitle={emptyTitle}\n        emptyDescription={emptyDescription}\n        emptyAction={emptyAction}\n        errorTitle={errorTitle}\n        errorDescription={errorDescription}\n        onRetry={onRetry}\n        loadingLabel={loadingLabel}\n        loadingRows={loadingRows}\n      >\n        {children}\n      </FeedBody>\n      </NotificationListContext.Provider>\n    </section>\n  );\n  },\n);\nNotificationList.displayName = 'NotificationList';\n\nexport interface NotificationGroupProps extends HTMLAttributes<HTMLDivElement> {\n  // Day label, e.g. \"Today\".\n  label: string;\n}\n\nconst NotificationGroup = forwardRef<HTMLDivElement, NotificationGroupProps>(({ className, label, children, ...props }, ref) => {\n  const zero = usePreset() === 'zero';\n  return (\n  <div ref={ref} role=\"group\" aria-label={label} className={className} {...props}>\n    <p aria-hidden=\"true\" className={cn('px-3 py-1', zero ? 'typography-detail text-text-subtle' : 'typography-text-extra-small bg-muted text-muted-foreground')}>\n      {label}\n    </p>\n    <NotificationListContext.Provider value=\"list\">\n      <ul className=\"m-0 flex list-none flex-col p-0\">{children}</ul>\n    </NotificationListContext.Provider>\n  </div>\n  );\n});\nNotificationGroup.displayName = 'NotificationGroup';\n\nexport interface NotificationItemProps extends Omit<LiHTMLAttributes<HTMLLIElement>, 'title'> {\n  title: ReactNode;\n  meta?: ReactNode;\n  // Relative or absolute time, shown on the meta line after the meta text.\n  time?: ReactNode;\n  // Slot for an avatar.\n  leading?: ReactNode;\n  // Slot at the end of the row, e.g. a status chip, a menu button or an unread count.\n  trailing?: ReactNode;\n  unread?: boolean;\n  // Inline actions, e.g. a \"Reply\" button.\n  children?: ReactNode;\n}\n\nconst NotificationItem = forwardRef<HTMLLIElement, NotificationItemProps>(({ className, title, meta, time, leading, trailing, unread = false, children, ...props }, ref) => {\n  const l = useLabels();\n  // Zero: the time sits in its own right column and the unread dot follows the title.\n  const zero = usePreset() === 'zero';\n  const Item = (useContext(NotificationListContext) === 'list' ? 'li' : 'div') as 'li';\n  return (\n  <Item\n    ref={ref}\n    data-unread={unread || undefined}\n    className={cn('typography-ui relative flex items-start gap-3 border-b border-solid border-hairline px-3 py-3 last:border-b-0', className)}\n    {...props}\n  >\n    {unread ? (\n      <>\n        {zero ? null : <span aria-hidden=\"true\" className=\"absolute left-1 top-4 h-1 w-1 rounded-full bg-icon-brand\" />}\n        <span className=\"sr-only\">{l.unreadPrefix}</span>\n      </>\n    ) : null}\n    {leading}\n    <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n      <p className={cn(unread ? 'typography-ui-medium' : 'typography-ui', 'text-foreground', zero && unread && 'flex items-center gap-2')}>\n        {title}\n        {zero && unread ? <span aria-hidden=\"true\" data-unread-dot=\"\" className=\"h-2 w-2 shrink-0 rounded-full bg-background-brand-bold-default\" /> : null}\n      </p>\n      {meta || (time && !zero) ? (\n        <p className=\"typography-text-extra-small flex flex-wrap items-baseline gap-x-2 text-muted-foreground\">\n          {meta ? <span>{meta}</span> : null}\n          {time && !zero ? <span className=\"whitespace-nowrap\">{time}</span> : null}\n        </p>\n      ) : null}\n      {children ? <div className=\"mt-1 flex flex-wrap items-center gap-2\">{children}</div> : null}\n    </div>\n    {zero && time ? <span data-notification-time=\"\" className=\"typography-text-extra-small shrink-0 self-start whitespace-nowrap text-text-subtle\">{time}</span> : null}\n    {trailing ? (\n      <div data-notification-trailing=\"\" className=\"flex shrink-0 items-center gap-2 self-start\">\n        {trailing}\n      </div>\n    ) : null}\n  </Item>\n  );\n});\nNotificationItem.displayName = 'NotificationItem';\n\nexport { MilestoneTimeline, NotificationGroup, NotificationItem, NotificationList, Timeline, TimelineItem };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}