{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "empty-states",
  "title": "Empty states",
  "description": "The four things an empty view can mean, each with one way forward: nothing created yet, nothing matching a filter, nothing deployed, and a load that failed with a retry.",
  "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/code-block.json",
    "https://sproutboat.com/ikada/r/empty-state.json",
    "https://sproutboat.com/ikada/r/icons.json",
    "https://sproutboat.com/ikada/r/input.json"
  ],
  "files": [
    {
      "path": "registry/ikada/blocks/empty-states/empty-states.tsx",
      "content": "\"use client\";\n\n/**\n * The four things an empty view can mean, each with one way forward: nothing created yet, nothing\n * matching a filter, nothing deployed, and a load that failed with a retry.\n */\nimport { useState } from \"react\";\nimport { Button } from \"@/components/ikada/button\";\nimport { CodeBlock } from \"@/components/ikada/code-block\";\nimport { EmptyState } from \"@/components/ikada/empty-state\";\nimport { Input } from \"@/components/ikada/input\";\nimport { cn } from \"@/components/ikada/lib/cn\";\nimport { AlertIcon, FolderIcon, RefreshIcon, RocketIcon, SearchIcon } from \"@/components/ikada/lib/icons\";\n\n/** First run: nothing exists yet. Says what the list will hold and offers the one action that fills it. */\nexport function NoProjectsYet({ onCreate = () => {} }: { onCreate?: () => void }) {\n  return (\n    <div className=\"flex flex-col justify-center rounded-xl bg-surface shadow-card\">\n      <EmptyState\n        icon={<FolderIcon />}\n        title=\"Create your first project\"\n        description=\"A project groups the sprouts that ship together, like a site and its API.\"\n        action={\n          <Button variant=\"primary\" onClick={onCreate}>\n            New project\n          </Button>\n        }\n        className=\"py-12\"\n      />\n    </div>\n  );\n}\n\nconst SAMPLE_ROWS = [\"pond-web\", \"pond-api\", \"nightly-digest\", \"thumbnailer\", \"lily-pad-web\"];\n\n/**\n * A filter that matches nothing: the list stays in place, the empty line names the query and\n * clearing it is one click. Here the search runs over a few sample names; try \"reed\".\n */\nexport function NoResults({ rows = SAMPLE_ROWS, initialQuery = \"reed\" }: { rows?: string[]; initialQuery?: string }) {\n  const [query, setQuery] = useState(initialQuery);\n  const q = query.trim().toLowerCase();\n  const shown = rows.filter((r) => r.includes(q));\n  return (\n    <div className=\"flex flex-col overflow-hidden rounded-xl bg-surface shadow-card\">\n      <div className=\"relative border-b border-hairline p-3\">\n        <SearchIcon className=\"pointer-events-none absolute start-6 top-1/2 -translate-y-1/2 text-fg-muted\" />\n        <Input\n          type=\"search\"\n          aria-label=\"Search sprouts\"\n          placeholder=\"Search sprouts\"\n          value={query}\n          onChange={(e) => setQuery(e.target.value)}\n          className=\"ps-9\"\n        />\n      </div>\n      {shown.length ? (\n        <ul className=\"divide-y divide-hairline\">\n          {shown.map((r) => (\n            <li key={r} className=\"px-4 py-2.5 font-mono text-sb-sm\">\n              {r}\n            </li>\n          ))}\n        </ul>\n      ) : (\n        <EmptyState\n          icon={<SearchIcon />}\n          title=\"Nothing matches this search\"\n          description={\n            <>\n              No sprout name contains “{query.trim()}”. Check the spelling or clear the search to see all {rows.length}.\n            </>\n          }\n          action={\n            <Button variant=\"secondary\" size=\"sm\" onClick={() => setQuery(\"\")}>\n              Clear search\n            </Button>\n          }\n        />\n      )}\n    </div>\n  );\n}\n\n/** Created but never deployed: the next step is a command, so the empty state hands it over to copy. */\nexport function NothingDeployed({ command = \"npx sproutboat deploy\" }: { command?: string }) {\n  return (\n    <div className=\"flex flex-col justify-center rounded-xl bg-surface shadow-card\">\n      <EmptyState\n        icon={<RocketIcon />}\n        title=\"Nothing deployed yet\"\n        description=\"From the folder with your code, deploy once and this sprout goes live with its own URL.\"\n        action={<CodeBlock label=\"Deploy command\" code={command} className=\"w-64 max-w-full text-start\" />}\n        className=\"py-12\"\n      />\n    </div>\n  );\n}\n\n/**\n * A load that failed: say what could not load and why in plain words, and offer a retry. The\n * sample retry takes a moment and fails again, so the loading state can be seen.\n */\nexport function LoadFailed({\n  what = \"your projects\",\n  onRetry,\n}: {\n  what?: string;\n  /** Retries the load. Defaults to a sample that waits a second. */\n  onRetry?: () => Promise<void> | void;\n}) {\n  const [retrying, setRetrying] = useState(false);\n  const [attempts, setAttempts] = useState(0);\n  const retry = async () => {\n    setRetrying(true);\n    try {\n      await (onRetry ? onRetry() : new Promise<void>((r) => setTimeout(r, 1000)));\n    } finally {\n      setRetrying(false);\n      setAttempts((a) => a + 1);\n    }\n  };\n  return (\n    <div className=\"flex flex-col justify-center rounded-xl bg-surface shadow-card\" role=\"alert\">\n      <EmptyState\n        icon={<AlertIcon className=\"text-danger-fg\" />}\n        title={`Could not load ${what}`}\n        description={\n          <>{attempts ? `Still unreachable after ${attempts + 1} tries. ` : \"\"}Check your connection, then try again.</>\n        }\n        action={\n          <Button variant=\"secondary\" loading={retrying} onClick={() => void retry()}>\n            <RefreshIcon />\n            Try again\n          </Button>\n        }\n        className=\"py-12\"\n      />\n    </div>\n  );\n}\n\nexport interface EmptyStatesProps {\n  className?: string;\n  /** What \"New project\" does in the first-run state. */\n  onCreate?: () => void;\n}\n\n// All four as a gallery: keep the one your view needs and delete the rest.\nexport function EmptyStates({ className, onCreate }: EmptyStatesProps) {\n  const items = [\n    { id: \"first\", title: \"First run\", body: <NoProjectsYet onCreate={onCreate} /> },\n    { id: \"filter\", title: \"No results\", body: <NoResults /> },\n    { id: \"deploy\", title: \"Nothing deployed\", body: <NothingDeployed /> },\n    { id: \"error\", title: \"Load failed\", body: <LoadFailed /> },\n  ];\n  return (\n    <div className={cn(\"@container\", className)}>\n      <div className=\"grid grid-cols-1 gap-5 @2xl:grid-cols-2\">\n        {items.map((item) => (\n          <section key={item.id} aria-labelledby={`empty-${item.id}`} className=\"flex min-w-0 flex-col gap-2\">\n            <h2 id={`empty-${item.id}`} className=\"px-1 text-sb-sm font-semibold text-fg-muted\">\n              {item.title}\n            </h2>\n            <div className=\"flex flex-1 flex-col *:flex-1\">{item.body}</div>\n          </section>\n        ))}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ikada/blocks/empty-states/empty-states.tsx"
    }
  ],
  "categories": [
    "dashboard"
  ],
  "type": "registry:block"
}