{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-card",
  "type": "registry:ui",
  "title": "Task Card",
  "description": "Layered task cards with colored priority bands, recessed dashed outlines, stacked avatars and soft shadows.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "@radix-ui/react-avatar",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils",
    "avatar"
  ],
  "files": [
    {
      "path": "src/registry/ui/task-card.tsx",
      "content": "\"use client\";\n\nimport { useId, useRef, type CSSProperties } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { FolderClosed, Link2, MessagesSquare } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { cn } from \"@/lib/utils\";\n\ngsap.registerPlugin(useGSAP);\n\nexport const TASK_CARD_PRIORITIES = {\n  moderate: { label: \"Moderate priority\", color: \"#ff7415\" },\n  urgent: { label: \"Urgent\", color: \"#fa454b\" },\n  low: { label: \"Low priority\", color: \"#36ad71\" },\n  onboarding: { label: \"On boarding\", color: \"#47b3f5\" },\n} as const;\n\nexport type TaskCardPriority = keyof typeof TASK_CARD_PRIORITIES;\n\nexport interface TaskCardAssignee {\n  name: string;\n  src?: string;\n}\n\nexport interface TaskCardProps {\n  title: string;\n  description?: string;\n  priority?: TaskCardPriority;\n  priorityLabel?: string;\n  priorityColor?: string;\n  status?: string;\n  assignees?: TaskCardAssignee[];\n  maxAssignees?: number;\n  comments?: number;\n  links?: number;\n  files?: number;\n  dueDate?: string;\n  theme?: \"dark\" | \"light\";\n  radius?: number;\n  padding?: number;\n  shadow?: number;\n  hoverLift?: number;\n  avatarSize?: number;\n  bandHeight?: number;\n  dashedBorder?: boolean;\n  showAssignees?: boolean;\n  showStatus?: boolean;\n  showFooter?: boolean;\n  surfaceColor?: string;\n  textColor?: string;\n  className?: string;\n  onOpen?: () => void;\n}\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nexport function TaskCard({\n  title,\n  description,\n  priority = \"moderate\",\n  priorityLabel,\n  priorityColor,\n  status = \"In Progress\",\n  assignees = [],\n  maxAssignees = 4,\n  comments = 8,\n  links = 4,\n  files = 12,\n  dueDate,\n  theme = \"dark\",\n  radius = 20,\n  padding = 18,\n  shadow = 0.7,\n  hoverLift = 3,\n  avatarSize = 30,\n  bandHeight = 38,\n  dashedBorder = true,\n  showAssignees = true,\n  showStatus = true,\n  showFooter = true,\n  surfaceColor,\n  textColor,\n  className,\n  onOpen,\n}: TaskCardProps) {\n  const rootRef = useRef<HTMLElement>(null);\n  const titleId = useId();\n  const lift = bounded(hoverLift, 3, 0, 10);\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      if (!root || !lift) return;\n      const media = gsap.matchMedia();\n      media.add(\"(prefers-reduced-motion: no-preference)\", () => {\n        const hover = window.matchMedia(\"(hover: hover)\");\n        const motion = gsap.to(root, {\n          y: -lift,\n          duration: 0.35,\n          ease: \"power3.out\",\n          paused: true,\n        });\n        const enter = () => {\n          if (hover.matches) motion.play();\n        };\n        const focus = () => motion.play();\n        const leave = () => {\n          if (!root.contains(document.activeElement)) motion.reverse();\n        };\n        const blur = () => {\n          if (!root.matches(\":hover\")) motion.reverse();\n        };\n        root.addEventListener(\"pointerenter\", enter);\n        root.addEventListener(\"pointerleave\", leave);\n        root.addEventListener(\"focusin\", focus);\n        root.addEventListener(\"focusout\", blur);\n        return () => {\n          root.removeEventListener(\"pointerenter\", enter);\n          root.removeEventListener(\"pointerleave\", leave);\n          root.removeEventListener(\"focusin\", focus);\n          root.removeEventListener(\"focusout\", blur);\n        };\n      });\n      return () => media.revert();\n    },\n    { scope: rootRef, dependencies: [lift], revertOnUpdate: true },\n  );\n\n  const light = theme === \"light\";\n  const depth = bounded(shadow, 0.7, 0, 1);\n  const curve = bounded(radius, 20, 8, 36);\n  const inset = bounded(padding, 18, 12, 32);\n  const size = bounded(avatarSize, 30, 20, 44);\n  const limit = Math.floor(bounded(maxAssignees, 4, 1, 8));\n  const date = dueDate ? new Date(`${dueDate}T00:00:00Z`) : null;\n  const validDate = date && Number.isFinite(date.getTime());\n  const dateLabel = validDate\n    ? new Intl.DateTimeFormat(\"en-GB\", {\n        day: \"2-digit\",\n        month: \"2-digit\",\n        year: \"numeric\",\n        timeZone: \"UTC\",\n      }).format(date)\n    : undefined;\n  const base = TASK_CARD_PRIORITIES[priority] ?? TASK_CARD_PRIORITIES.moderate;\n  const visibleAssignees = assignees.slice(0, limit);\n  const overflow = assignees.length - visibleAssignees.length;\n\n  return (\n    <article\n      ref={rootRef}\n      aria-labelledby={titleId}\n      data-slot=\"task-card\"\n      data-priority={priority}\n      className={cn(\"relative isolate w-full min-w-0 font-sans\", className)}\n      style={\n        {\n          \"--task-radius\": `${curve}px`,\n          \"--task-inset\": `${inset}px`,\n          \"--task-surface\": surfaceColor ?? (light ? \"#ffffff\" : \"#202020\"),\n          \"--task-text\": textColor ?? (light ? \"#202124\" : \"#f4f4f4\"),\n          \"--task-muted\": light ? \"#74777c\" : \"#96989b\",\n          \"--task-border\": light ? \"#d4d5d8\" : \"#555658\",\n          \"--task-status\": light ? \"#e7e7e8\" : \"#474747\",\n          borderRadius: curve,\n          background: light ? \"#f2f2f3\" : \"#171717\",\n          border: `1px solid ${light ? \"#dddddf\" : \"#292929\"}`,\n          boxShadow: `0 ${12 * depth}px ${28 * depth}px -${8 * depth}px rgb(0 0 0 / ${depth * (light ? 0.16 : 0.4)}), 0 2px 4px rgb(0 0 0 / ${depth * 0.16}), inset 0 1px 0 rgb(255 255 255 / ${depth * 0.06})`,\n          color: \"var(--task-text)\",\n        } as CSSProperties\n      }\n    >\n      <div\n        data-slot=\"task-card-priority\"\n        className=\"flex items-start justify-center overflow-hidden px-3 pt-1.5 pb-3 text-center text-[13px] font-semibold uppercase tracking-[0.045em] text-white\"\n        style={{\n          minHeight: bounded(bandHeight, 38, 38, 60),\n          borderRadius: `${curve - 1}px ${curve - 1}px 0 0`,\n          background: `linear-gradient(105deg, color-mix(in srgb, ${priorityColor ?? base.color}, white 4%), ${priorityColor ?? base.color})`,\n          textShadow: \"0 1px 2px rgb(0 0 0 / 0.15)\",\n        }}\n      >\n        <span className=\"min-w-0 line-clamp-2 wrap-anywhere\">\n          {priorityLabel ?? base.label}\n        </span>\n      </div>\n      <div\n        className=\"relative -mt-3 rounded-(--task-radius) bg-(--task-surface)\"\n        style={{ padding: inset, boxShadow: \"0 -1px 0 rgb(0 0 0 / 0.2)\" }}\n      >\n        {dashedBorder && (\n          <span\n            data-slot=\"task-card-outline\"\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-1 border border-dashed border-(--task-border)\"\n            style={{ borderRadius: Math.max(4, curve - 4) }}\n          />\n        )}\n        <h3\n          id={titleId}\n          className=\"text-[19px] leading-snug font-medium tracking-[-0.04em] wrap-anywhere\"\n        >\n          {onOpen ? (\n            <button\n              type=\"button\"\n              onClick={onOpen}\n              className=\"cursor-pointer rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current\"\n              aria-label={`Open ${title || \"Untitled task\"}`}\n            >\n              {title || \"Untitled task\"}\n            </button>\n          ) : (\n            title || \"Untitled task\"\n          )}\n        </h3>\n        {description && (\n          <p className=\"mt-2.5 line-clamp-2 text-[15px] leading-[1.45] tracking-[-0.025em] text-(--task-muted) wrap-anywhere\">\n            {description}\n          </p>\n        )}\n        {((showAssignees && assignees.length > 0) ||\n          (showStatus && status)) && (\n          <div className=\"mt-4 flex min-w-0 flex-wrap items-center justify-between gap-x-3 gap-y-2\">\n            {showAssignees && assignees.length > 0 && (\n              <ul aria-label=\"Assignees\" className=\"flex -space-x-3\">\n                {visibleAssignees.map((person, index) => (\n                  <li key={`${person.name}-${index}`} title={person.name}>\n                    <Avatar\n                      className=\"border-[1.5px] border-white bg-zinc-300 ring-[1.5px] ring-black\"\n                      style={{ width: size, height: size }}\n                    >\n                      <AvatarImage\n                        src={person.src}\n                        alt={person.name}\n                        className=\"object-cover\"\n                      />\n                      <AvatarFallback\n                        className=\"bg-zinc-300 text-[10px] font-semibold text-zinc-800\"\n                        aria-label={person.name}\n                      >\n                        {person.name\n                          .split(/\\s+/)\n                          .filter(Boolean)\n                          .slice(0, 2)\n                          .map((word) => word[0])\n                          .join(\"\") || \"?\"}\n                      </AvatarFallback>\n                    </Avatar>\n                  </li>\n                ))}\n                {overflow > 0 && (\n                  <li\n                    className=\"relative flex items-center justify-center rounded-full border border-white bg-zinc-700 text-[10px] font-medium text-white ring-[1.5px] ring-black\"\n                    style={{ width: size, height: size }}\n                    aria-label={`${overflow} more assignees: ${assignees\n                      .slice(limit)\n                      .map((person) => person.name)\n                      .join(\", \")}`}\n                  >\n                    +{overflow}\n                  </li>\n                )}\n              </ul>\n            )}\n            {showStatus && status && (\n              <span className=\"ml-auto max-w-full rounded-[5px] bg-(--task-status) px-2.5 py-1.5 text-[13px] leading-snug tracking-[-0.03em] wrap-anywhere\">\n                {status}\n              </span>\n            )}\n          </div>\n        )}\n      </div>\n      {showFooter && (\n        <footer\n          className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-2 py-3 text-[16px] tabular-nums\"\n          style={{ paddingInline: inset }}\n        >\n          <div className=\"flex items-center gap-4\">\n            {[\n              { Icon: MessagesSquare, value: comments, label: \"comments\" },\n              { Icon: Link2, value: links, label: \"links\" },\n              { Icon: FolderClosed, value: files, label: \"files\" },\n            ].map(({ Icon, value, label }) => (\n              <span\n                key={label}\n                className=\"flex items-center gap-1.5\"\n                aria-label={`${Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0} ${label}`}\n              >\n                <Icon\n                  size={17}\n                  strokeWidth={1.6}\n                  className=\"text-(--task-muted)\"\n                  aria-hidden=\"true\"\n                />\n                <span aria-hidden=\"true\">\n                  {Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0}\n                </span>\n              </span>\n            ))}\n          </div>\n          {dateLabel && (\n            <time\n              dateTime={dueDate}\n              className=\"ml-auto text-(--task-muted)\"\n              aria-label={`Due ${dateLabel}`}\n            >\n              {dateLabel}\n            </time>\n          )}\n        </footer>\n      )}\n    </article>\n  );\n}\n\nexport default TaskCard;\n",
      "type": "registry:ui",
      "target": "@ui/task-card.tsx"
    }
  ]
}
