{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "project-tray",
  "title": "Project tray",
  "description": "A recessed tray per project holding its sprouts as floating cards: each card has a status tile, the hostname and one state line, and an empty project offers to plant its first sprout.",
  "registryDependencies": [
    "https://sproutboat.com/ikada/r/theme.json",
    "https://sproutboat.com/ikada/r/button.json",
    "https://sproutboat.com/ikada/r/cn.json",
    "https://sproutboat.com/ikada/r/format.json",
    "https://sproutboat.com/ikada/r/icons.json",
    "https://sproutboat.com/ikada/r/status.json",
    "https://sproutboat.com/ikada/r/status-dot.json"
  ],
  "files": [
    {
      "path": "registry/ikada/blocks/project-tray/project-tray.tsx",
      "content": "\"use client\";\n\n/**\n * A recessed tray per project holding its sprouts as floating cards: each card has a status tile,\n * the hostname and one state line, and an empty project offers to plant its first sprout.\n */\nimport { buttonStyles } from \"@/components/ikada/button\";\nimport { StatusDot } from \"@/components/ikada/status-dot\";\nimport type { Status } from \"@/components/ikada/status\";\nimport { cn, focusRing } from \"@/components/ikada/lib/cn\";\nimport { formatCompact } from \"@/components/ikada/lib/format\";\nimport { ChevronRightIcon, PlusIcon } from \"@/components/ikada/lib/icons\";\n\nexport type SproutStatus = \"live\" | \"deploying\" | \"deploy_failed\" | \"paused\" | \"not_deployed\";\n\nexport interface Sprout {\n  slug: string;\n  name: string;\n  hostname: string;\n  status: SproutStatus;\n  /** What wakes it: \"http\" for a web sprout, or \"cron\", \"queue\". Anything but http shows as a tag. */\n  trigger?: string;\n  /** The version serving traffic, if any. */\n  liveVersion?: number;\n  /** When the last deploy finished, already phrased: \"12 min ago\". */\n  lastDeploy?: string;\n  requests24h?: number;\n}\n\nexport interface Project {\n  slug: string;\n  name: string;\n  sprouts: Sprout[];\n}\n\nexport interface ProjectTrayProps {\n  projects?: Project[];\n  /** Link to a project page. */\n  projectHref?: (project: Project) => string;\n  /** Link to a sprout page. */\n  sproutHref?: (project: Project, sprout: Sprout) => string;\n  /** Called by the \"Sprout\" button and the empty tray. */\n  onNewSprout?: (project: Project) => void;\n  className?: string;\n}\n\n/** Sample data: two projects with a mix of states, and a third that is still empty. */\nexport const sampleProjects: Project[] = [\n  {\n    slug: \"pond\",\n    name: \"Pond\",\n    sprouts: [\n      {\n        slug: \"pond-web\",\n        name: \"pond-web\",\n        hostname: \"pond-web.lily.sproutboat.app\",\n        status: \"live\",\n        liveVersion: 48,\n        lastDeploy: \"12 min ago\",\n        requests24h: 184_200,\n      },\n      {\n        slug: \"pond-api\",\n        name: \"pond-api\",\n        hostname: \"pond-api.lily.sproutboat.app\",\n        status: \"deploying\",\n        liveVersion: 31,\n        requests24h: 96_400,\n      },\n      {\n        slug: \"nightly-digest\",\n        name: \"nightly-digest\",\n        hostname: \"nightly-digest.lily.sproutboat.app\",\n        status: \"live\",\n        trigger: \"cron\",\n        liveVersion: 7,\n        lastDeploy: \"3 days ago\",\n      },\n      {\n        slug: \"thumbnailer\",\n        name: \"thumbnailer\",\n        hostname: \"thumbnailer.lily.sproutboat.app\",\n        status: \"deploy_failed\",\n        trigger: \"queue\",\n        liveVersion: 12,\n      },\n    ],\n  },\n  {\n    slug: \"lily-pad\",\n    name: \"Lily pad\",\n    sprouts: [\n      {\n        slug: \"lily-pad-web\",\n        name: \"lily-pad-web\",\n        hostname: \"lily-pad-web.lily.sproutboat.app\",\n        status: \"live\",\n        liveVersion: 5,\n        lastDeploy: \"yesterday\",\n        requests24h: 2_310,\n      },\n      {\n        slug: \"lily-pad-preview\",\n        name: \"lily-pad-preview\",\n        hostname: \"lily-pad-preview.lily.sproutboat.app\",\n        status: \"paused\",\n      },\n    ],\n  },\n  { slug: \"reed\", name: \"Reed\", sprouts: [] },\n];\n\nconst look: Record<SproutStatus, { status: Status; live?: boolean; pulse?: boolean }> = {\n  live: { status: \"success\", live: true },\n  deploying: { status: \"info\", pulse: true },\n  deploy_failed: { status: \"danger\" },\n  paused: { status: \"neutral\" },\n  not_deployed: { status: \"neutral\" },\n};\n\n/** \"Live · deployed 12 min ago\", \"Deploy failed · v12 serving\": the one line a card needs. */\nexport function stateLine(s: Sprout): string {\n  switch (s.status) {\n    case \"live\":\n      return s.lastDeploy ? `Live · deployed ${s.lastDeploy}` : \"Live\";\n    case \"deploying\":\n      return s.liveVersion ? `Deploying · v${s.liveVersion} still live` : \"Deploying the first version\";\n    case \"deploy_failed\":\n      return s.liveVersion ? `Deploy failed · v${s.liveVersion} serving` : \"Deploy failed\";\n    case \"paused\":\n      return \"Paused · answers 503\";\n    case \"not_deployed\":\n      return \"Not deployed yet\";\n  }\n}\n\n/** The sprout leaf on a soft tile, tinted by status. */\nfunction SproutTile({ status }: { status: Status }) {\n  const surface = {\n    success: \"bg-brand-soft text-brand-fg\",\n    info: \"bg-info-soft text-info-fg\",\n    danger: \"bg-danger-soft text-danger-fg\",\n    warning: \"bg-warn-soft text-warn-fg\",\n    neutral: \"bg-neutral-soft text-fg-muted\",\n  }[status];\n  return (\n    <span aria-hidden=\"true\" className={cn(\"grid size-7 shrink-0 place-items-center rounded-lg\", surface)}>\n      <svg viewBox=\"3 6 24 25\" className=\"size-4\" fill=\"none\">\n        <path d=\"M16 29V15\" stroke=\"currentColor\" strokeWidth=\"2.8\" strokeLinecap=\"round\" />\n        <path\n          d=\"M16 16.5c0-4.7-3.8-8.5-8.5-8.5C6.7 8 6 8.7 6 9.5 6 14.2 9.8 18 14.5 18c.8 0 1.5-.7 1.5-1.5Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M16 15c0-4 3.2-7.2 7.2-7.2.7 0 1.3.6 1.3 1.3C24.5 12.8 21.3 16 17.3 16c-.7 0-1.3-.6-1.3-1Z\"\n          fill=\"currentColor\"\n          fillOpacity=\"0.72\"\n        />\n      </svg>\n    </span>\n  );\n}\n\nfunction SproutCard({ sprout, href }: { sprout: Sprout; href: string }) {\n  const l = look[sprout.status];\n  return (\n    <a\n      href={href}\n      className={cn(\n        \"flex min-w-0 flex-col gap-2.5 rounded-[0.6875rem] bg-surface p-3.5 pb-3 text-fg shadow-card\",\n        \"transition-shadow duration-(--sb-duration-hover) ease-hover hover:shadow-float\",\n        focusRing,\n      )}\n    >\n      <span className=\"flex min-w-0 items-center gap-2\">\n        <SproutTile status={l.status} />\n        <span className=\"min-w-0 flex-1 truncate text-sb-base font-semibold\" title={sprout.name}>\n          {sprout.name}\n        </span>\n        {sprout.trigger && sprout.trigger !== \"http\" ? (\n          <span\n            title={sprout.trigger}\n            className=\"max-w-[40%] shrink-0 truncate rounded-md bg-neutral-soft px-1.5 py-0.5 font-mono text-sb-xs font-semibold text-neutral-fg\"\n          >\n            {sprout.trigger}\n          </span>\n        ) : null}\n      </span>\n      <span className=\"truncate font-mono text-sb-xs text-fg-muted\" title={sprout.hostname}>\n        {sprout.hostname}\n      </span>\n      <span className=\"flex min-w-0 border-t border-hairline pt-2.5\">\n        <StatusDot\n          tone={l.status}\n          live={l.live}\n          pulse={l.pulse}\n          label={stateLine(sprout)}\n          className={cn(\n            \"min-w-0 truncate text-sb-sm\",\n            sprout.status === \"deploy_failed\" ? \"text-danger-fg\" : \"text-fg-muted\",\n          )}\n        />\n      </span>\n    </a>\n  );\n}\n\nconst grid = \"grid grid-cols-[repeat(auto-fill,minmax(12.75rem,1fr))] gap-1.5\";\n\nfunction Tray({\n  project,\n  href,\n  sproutHref,\n  onNewSprout,\n}: {\n  project: Project;\n  href: string;\n  sproutHref: (sprout: Sprout) => string;\n  onNewSprout: () => void;\n}) {\n  const requests = project.sprouts.reduce((sum, s) => sum + (s.requests24h ?? 0), 0);\n  const id = `tray-${project.slug}`;\n  const count = project.sprouts.length;\n  return (\n    <section\n      aria-labelledby={id}\n      className=\"@container rounded-2xl bg-well p-1.5 shadow-[inset_0_0_0_1px_var(--sb-hairline)]\"\n    >\n      <div className=\"flex min-w-0 items-center gap-2.5 px-3 pt-2 pb-2.5\">\n        <h2 id={id} title={project.name} className=\"min-w-0 truncate text-sb-md font-semibold\">\n          <a href={href} className={cn(\"rounded-sm hover:underline\", focusRing)}>\n            {project.name}\n          </a>\n        </h2>\n        <span className=\"shrink-0 text-sb-sm text-fg-muted\">\n          {count} {count === 1 ? \"sprout\" : \"sprouts\"}\n        </span>\n        <span className=\"ms-auto flex shrink-0 items-center gap-1\">\n          {requests > 0 ? (\n            <span className=\"me-2 hidden text-sb-sm text-fg-muted tabular-nums @xl:inline\">\n              {formatCompact(requests)} requests, 24 h\n            </span>\n          ) : null}\n          <button\n            type=\"button\"\n            onClick={onNewSprout}\n            className={buttonStyles({ variant: \"ghost\", size: \"sm\" })}\n            aria-label={`New sprout in ${project.name}`}\n          >\n            <PlusIcon />\n            Sprout\n          </button>\n          <a\n            href={href}\n            className={cn(buttonStyles({ variant: \"ghost\", size: \"sm\" }), \"size-(--sb-control-h-sm) px-0\")}\n            aria-label={`Open ${project.name}`}\n          >\n            <ChevronRightIcon />\n          </a>\n        </span>\n      </div>\n      {count ? (\n        <div className={grid}>\n          {project.sprouts.map((s) => (\n            <SproutCard key={s.slug} sprout={s} href={sproutHref(s)} />\n          ))}\n        </div>\n      ) : (\n        <button\n          type=\"button\"\n          onClick={onNewSprout}\n          className={cn(\n            \"flex min-h-24 w-full flex-col items-center justify-center gap-1 rounded-[0.6875rem] p-4 text-center text-sb-sm text-fg-muted\",\n            \"border border-dashed border-hairline-strong\",\n            \"transition-colors duration-(--sb-duration-hover) ease-hover hover:bg-hover hover:text-fg\",\n            focusRing,\n          )}\n        >\n          <span className=\"inline-flex items-center gap-1.5 font-semibold text-fg\">\n            <PlusIcon />\n            Plant the first sprout\n          </span>\n          Start from a template, a repository or the CLI.\n        </button>\n      )}\n    </section>\n  );\n}\n\nexport function ProjectTray({\n  projects = sampleProjects,\n  projectHref = (p) => `#${p.slug}`,\n  sproutHref = (p, s) => `#${p.slug}/${s.slug}`,\n  onNewSprout = () => {},\n  className,\n}: ProjectTrayProps) {\n  return (\n    <div className={cn(\"flex flex-col gap-4\", className)}>\n      {projects.map((project) => (\n        <Tray\n          key={project.slug}\n          project={project}\n          href={projectHref(project)}\n          sproutHref={(s) => sproutHref(project, s)}\n          onNewSprout={() => onNewSprout(project)}\n        />\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ikada/blocks/project-tray/project-tray.tsx"
    }
  ],
  "categories": [
    "dashboard"
  ],
  "type": "registry:block"
}