{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"image-studio","type":"registry:block","title":"Image Generation Studio","description":"A one-screen generation studio — prompt composer and model picker beside a feed of queued, staged, blocked and ready image results with variants.","author":"Scrim UI (https://scrimui.dev)","categories":["pattern"],"docs":"https://scrimui.dev/patterns/image-studio","dependencies":[],"registryDependencies":["https://scrimui.dev/r/prompt-input.json","https://scrimui.dev/r/model-selector.json","https://scrimui.dev/r/generated-media.json"],"files":[{"path":"src/showcase/patterns/image-studio/image-studio.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { PromptInput } from \"@/components/ui/prompt-input\";\nimport { ModelSelector } from \"@/components/ui/model-selector\";\nimport { GeneratedMediaResult, type MediaStatus } from \"@/components/ui/generated-media\";\n\n/** CSS-painted stand-in for a generated image — patterns ship no assets. */\nfunction ImageMock({ hue = 210 }: { hue?: number }) {\n  return (\n    <div\n      role=\"img\"\n      aria-label=\"Generated illustration: a mountain ridge at dusk\"\n      className=\"flex h-full min-h-[220px] w-full items-end p-4\"\n      style={{\n        background: `linear-gradient(180deg, hsl(${hue} 60% 75%) 0%, hsl(${hue} 55% 45%) 60%, hsl(${hue} 50% 25%) 100%)`,\n      }}\n    >\n      <svg viewBox=\"0 0 400 80\" className=\"w-full\" aria-hidden=\"true\">\n        <path d=\"M0 80 L90 20 L160 60 L240 10 L320 55 L400 30 L400 80 Z\" fill=\"hsl(0 0% 100% / 0.25)\" />\n        <path d=\"M0 80 L120 45 L220 70 L340 40 L400 60 L400 80 Z\" fill=\"hsl(0 0% 0% / 0.2)\" />\n      </svg>\n    </div>\n  );\n}\n\n/**\n * A one-screen image generation studio.\n *\n * The flow this pattern exists to show:\n *\n * 1. **The queue is visible.** A submitted prompt enters the feed as a\n *    queued card immediately — position stated, no spinner-and-nothing.\n * 2. **Generation is staged, then it settles.** Queued → generating (stage\n *    in words) → ready; the card never changes shape underneath the reader.\n * 3. **Results come back as variants.** One prompt, three takes — picking\n *    one is cheap and reversible.\n * 4. **Blocked and failed are different days.** The second generation in\n *    this script hits the content policy (rephrase, no retry); the third\n *    fails on the worker (retry, which then succeeds).\n * 5. **The prompt is the re-use path.** Clicking a result's prompt loads it\n *    back into the composer.\n */\n\ntype Result = {\n  id: number;\n  kind: \"image\";\n  status: MediaStatus;\n  prompt: string;\n  params: string[];\n  stage?: string;\n  queuePosition?: number;\n  variants: { id: string }[];\n  currentVariantId: string;\n  hue: number;\n  errorMessage?: string;\n  blockedReason?: string;\n};\n\nconst STAGES = [\"Reading the prompt…\", \"Composing the scene…\", \"Diffusing latents…\", \"Upsampling…\"];\n\nconst MODELS = [\n  { id: \"fable-image\", name: \"Fable Image 2\", hint: \"Quality\" },\n  { id: \"sketch\", name: \"Sketch Turbo\", hint: \"Fast drafts\" },\n];\n\nconst INITIAL_RESULTS: Result[] = [\n  {\n    id: 1,\n    kind: \"image\",\n    status: \"ready\",\n    prompt: \"A mountain ridge at dusk, soft gradient sky, minimal illustration\",\n    params: [\"1024×1024\", \"seed 4815\", \"illustration\"],\n    variants: [{ id: \"v1\" }, { id: \"v2\" }, { id: \"v3\" }],\n    currentVariantId: \"v1\",\n    hue: 210,\n  },\n];\n\nexport function ImageStudioPattern() {\n  const [results, setResults] = React.useState<Result[]>(INITIAL_RESULTS);\n  const [model, setModel] = React.useState(\"fable-image\");\n  const [draftPrompt, setDraftPrompt] = React.useState(\"\");\n  const idRef = React.useRef(2);\n  const timers = React.useRef<number[]>([]);\n\n  function patch(id: number, p: Partial<Result>) {\n    setResults((rs) => rs.map((r) => (r.id === id ? { ...r, ...p } : r)));\n  }\n\n  function schedule(id: number, fn: () => void, ms: number) {\n    timers.current.push(window.setTimeout(fn, ms));\n  }\n\n  /** The scripted outcomes: 1st new generation succeeds, 2nd is policy-\n      blocked, 3rd fails (its Retry succeeds), 4th+ succeed again. */\n  function runGeneration(prompt: string) {\n    const id = idRef.current++;\n    const attempt = results.length; // initial card counts as a past success\n    const outcome = attempt % 3 === 1 ? \"blocked\" : attempt % 3 === 2 ? \"failed\" : \"ready\";\n    const hue = [160, 330, 45, 260][id % 4];\n\n    const base: Result = {\n      id,\n      kind: \"image\",\n      status: \"queued\",\n      prompt,\n      params: [\"1024×1024\", `seed ${1000 + id * 37}`, model === \"sketch\" ? \"draft\" : \"illustration\"],\n      queuePosition: 2,\n      variants: [{ id: \"v1\" }, { id: \"v2\" }, { id: \"v3\" }],\n      currentVariantId: \"v1\",\n      hue,\n    };\n    setResults((rs) => [base, ...rs]);\n\n    schedule(id, () => patch(id, { status: \"generating\", stage: STAGES[0], queuePosition: undefined }), 1200);\n    STAGES.forEach((s, i) => schedule(id, () => patch(id, { stage: s }), 1200 + 700 * i));\n    const settleAt = 1200 + 700 * STAGES.length;\n    if (outcome === \"ready\") {\n      schedule(id, () => patch(id, { status: \"ready\", stage: undefined }), settleAt);\n    } else if (outcome === \"blocked\") {\n      schedule(\n        id,\n        () =>\n          patch(id, {\n            status: \"blocked\",\n            stage: undefined,\n            blockedReason: \"The prompt names a real public figure. Describe a fictional character or scene instead.\",\n          }),\n        settleAt,\n      );\n    } else {\n      schedule(\n        id,\n        () => patch(id, { status: \"failed\", stage: undefined, errorMessage: \"The worker ran out of memory mid-generation.\" }),\n        settleAt,\n      );\n    }\n  }\n\n  function retry(r: Result) {\n    patch(r.id, { status: \"generating\", stage: STAGES[1], errorMessage: undefined });\n    schedule(r.id, () => patch(r.id, { status: \"ready\", stage: undefined }), 1600);\n  }\n\n  function regenerate(r: Result) {\n    const order = [\"v1\", \"v2\", \"v3\"];\n    const next = order[(order.indexOf(r.currentVariantId) + 1) % order.length];\n    patch(r.id, { status: \"generating\", stage: STAGES[2], currentVariantId: next });\n    schedule(r.id, () => patch(r.id, { status: \"ready\", stage: undefined }), 1400);\n  }\n\n  return (\n    <div className=\"flex h-[640px] overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900\">\n      {/* Composer rail */}\n      <aside className=\"hidden w-72 shrink-0 flex-col gap-3 border-r border-zinc-200 p-3 dark:border-zinc-800 md:flex\">\n        <p className=\"text-[13px] font-semibold text-zinc-900 dark:text-zinc-100\">Image Studio</p>\n        <ModelSelector options={MODELS} value={model} onSelect={setModel} />\n        {draftPrompt && (\n          <p className=\"rounded-lg bg-zinc-50 px-2.5 py-2 text-[11px] leading-4 text-zinc-500 dark:bg-zinc-800/60 dark:text-zinc-400\">\n            Reusing: “{draftPrompt}”\n          </p>\n        )}\n        <div className=\"mt-auto\">\n          <PromptInput\n            placeholder=\"Describe the image…\"\n            onSubmit={(v) => {\n              runGeneration(v);\n              setDraftPrompt(\"\");\n            }}\n          />\n        </div>\n      </aside>\n\n      {/* Results feed */}\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        <div className=\"border-b border-zinc-200 px-4 py-3 dark:border-zinc-800 md:hidden\">\n          <PromptInput\n            placeholder=\"Describe the image…\"\n            onSubmit={(v) => runGeneration(v)}\n          />\n        </div>\n        <div className=\"flex-1 space-y-4 overflow-y-auto px-4 py-4\">\n          {results.map((r) => (\n            <GeneratedMediaResult\n              key={r.id}\n              kind={r.kind}\n              status={r.status}\n              prompt={r.prompt}\n              params={r.params}\n              stage={r.stage}\n              queuePosition={r.queuePosition}\n              variants={r.variants}\n              currentVariantId={r.currentVariantId}\n              onVariantChange={(vid) => patch(r.id, { currentVariantId: vid })}\n              errorMessage={r.errorMessage}\n              blockedReason={r.blockedReason}\n              onDownload={() => {}}\n              onRegenerate={() => regenerate(r)}\n              onRetry={() => retry(r)}\n              onCancel={() => patch(r.id, { status: \"cancelled\", stage: undefined })}\n            >\n              <ImageMock hue={r.hue + (r.currentVariantId === \"v2\" ? 30 : r.currentVariantId === \"v3\" ? -30 : 0)} />\n            </GeneratedMediaResult>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:block","target":"components/blocks/image-studio.tsx"}]}