{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "blog-template",
  "title": "Blog template",
  "author": "andongmin94",
  "description": "A focused blog archive with search and readable post rows.",
  "dependencies": [
    "lucide-react@^1.24.0"
  ],
  "registryDependencies": [
    "https://neobrutal-ui.andongmin.com/r/neobrutal-ui.json",
    "https://neobrutal-ui.andongmin.com/r/button.json",
    "https://neobrutal-ui.andongmin.com/r/input.json"
  ],
  "files": [
    {
      "path": "src/lib/blog-posts.ts",
      "content": "export type BlogPostSection = {\n  body: readonly string[];\n  heading: string;\n  points?: readonly string[];\n};\n\nexport type BlogPost = {\n  intro: string;\n  publishedAt: string;\n  publishedLabel: string;\n  readTime: string;\n  sections: readonly BlogPostSection[];\n  slug: string;\n  summary: string;\n  title: string;\n  topic: string;\n};\n\nexport const BLOG_POSTS = [\n  {\n    slug: \"small-interfaces\",\n    title: \"Why Small Interfaces Age Better\",\n    summary: \"A practical case for fewer controls, clearer defaults, and less maintenance.\",\n    topic: \"Design\",\n    publishedAt: \"2026-07-12\",\n    publishedLabel: \"Jul 12\",\n    readTime: \"5 min\",\n    intro:\n      \"Small interfaces are easier to understand on the first visit and easier to maintain on the hundredth release.\",\n    sections: [\n      {\n        heading: \"Start with the boundary\",\n        body: [\n          \"A useful interface begins by deciding what it will not ask the user to manage. Every exposed option becomes a promise the product has to explain, support, and preserve.\",\n          \"Keep the controls that change the outcome. Move rare configuration closer to the moment it becomes relevant instead of presenting it up front.\",\n        ],\n      },\n      {\n        heading: \"Make the default path obvious\",\n        body: [\n          \"A strong default removes a decision without hiding what happened. People should be able to complete the common task and still understand the system's choice.\",\n        ],\n        points: [\n          \"Name the primary action precisely.\",\n          \"Show the current state beside the control that changes it.\",\n          \"Keep advanced choices available without making them mandatory.\",\n        ],\n      },\n      {\n        heading: \"Leave room to remove things\",\n        body: [\n          \"Maintenance gets lighter when each control has a clear owner and measurable purpose. If nobody can explain why an option still exists, removing it may be the most useful design improvement.\",\n        ],\n      },\n    ],\n  },\n  {\n    slug: \"reading-a-codebase\",\n    title: \"Reading a Codebase Before Changing It\",\n    summary: \"The files and questions that reveal how an unfamiliar project really works.\",\n    topic: \"Engineering\",\n    publishedAt: \"2026-07-06\",\n    publishedLabel: \"Jul 6\",\n    readTime: \"8 min\",\n    intro:\n      \"The fastest way into an unfamiliar codebase is to follow one real workflow from its entry point to its visible result.\",\n    sections: [\n      {\n        heading: \"Trace one path end to end\",\n        body: [\n          \"Begin with a route, command, or event that you can reproduce. Follow the data through the components and services it touches, and note where validation and side effects happen.\",\n          \"This gives the directory structure a purpose. Names that looked abstract become parts of a concrete flow.\",\n        ],\n      },\n      {\n        heading: \"Find the contracts\",\n        body: [\n          \"Types, tests, schemas, and adapters describe the boundaries that other code already depends on. Read those before changing an implementation detail that appears local.\",\n        ],\n        points: [\n          \"Inputs accepted at the boundary\",\n          \"Outputs consumed by another module\",\n          \"Errors that are intentionally surfaced\",\n          \"Tests that describe behavior rather than implementation\",\n        ],\n      },\n      {\n        heading: \"Write a small change map\",\n        body: [\n          \"Before editing, list the files that own the behavior, the callers that rely on it, and the checks that will prove the change. The map can be short; its job is to prevent a local fix from becoming a hidden system change.\",\n        ],\n      },\n    ],\n  },\n  {\n    slug: \"useful-empty-states\",\n    title: \"Empty States Should Point Somewhere\",\n    summary: \"Five ways to turn a blank screen into a useful next step.\",\n    topic: \"Product\",\n    publishedAt: \"2026-06-28\",\n    publishedLabel: \"Jun 28\",\n    readTime: \"4 min\",\n    intro:\n      \"An empty state is not a pause in the product. It is the moment when the interface has the most responsibility to explain what comes next.\",\n    sections: [\n      {\n        heading: \"Explain why it is empty\",\n        body: [\n          \"No data can mean a first visit, a narrow filter, a failed request, or missing access. Use language that reflects the actual cause instead of showing the same generic message everywhere.\",\n        ],\n      },\n      {\n        heading: \"Offer one useful next step\",\n        body: [\n          \"The primary action should resolve the empty state or help the user understand it. Secondary links can provide examples or documentation, but they should not compete with the next action.\",\n        ],\n        points: [\n          \"Create the first item\",\n          \"Clear the active filters\",\n          \"Invite a teammate who owns the data\",\n          \"Retry a failed request\",\n          \"Open a relevant example\",\n        ],\n      },\n      {\n        heading: \"Keep the state compact\",\n        body: [\n          \"A blank screen does not need a marketing page inside it. A direct heading, one sentence, and a clear action usually provide enough context without slowing down repeat users.\",\n        ],\n      },\n    ],\n  },\n  {\n    slug: \"quiet-debugging\",\n    title: \"A Quieter Way to Debug Frontend Work\",\n    summary: \"A short loop for isolating layout, state, and network problems without guessing.\",\n    topic: \"Engineering\",\n    publishedAt: \"2026-06-19\",\n    publishedLabel: \"Jun 19\",\n    readTime: \"7 min\",\n    intro:\n      \"Debugging gets faster when each experiment answers one question and changes one variable.\",\n    sections: [\n      {\n        heading: \"Reduce the surface area\",\n        body: [\n          \"Reproduce the problem in the smallest route, viewport, and state you can. Disable unrelated animation and data refreshes so the signal does not move while you inspect it.\",\n        ],\n      },\n      {\n        heading: \"Use a short loop\",\n        body: [\n          \"State the current hypothesis before touching the code. Then run the smallest observation or change that could disprove it.\",\n        ],\n        points: [\n          \"Observe the DOM, computed styles, state, and request timing.\",\n          \"Change one variable.\",\n          \"Reproduce from the same starting state.\",\n          \"Keep or revert the change immediately.\",\n        ],\n      },\n      {\n        heading: \"Record the useful result\",\n        body: [\n          \"When the cause is subtle, leave a focused regression test or a short comment near the constraint. The goal is not to preserve the debugging story; it is to prevent the same ambiguity from returning.\",\n        ],\n      },\n    ],\n  },\n  {\n    slug: \"decision-notes\",\n    title: \"Write Down the Decision, Not the Meeting\",\n    summary: \"A tiny note format that preserves context without becoming more process.\",\n    topic: \"Workflow\",\n    publishedAt: \"2026-06-11\",\n    publishedLabel: \"Jun 11\",\n    readTime: \"6 min\",\n    intro:\n      \"A useful decision note lets someone understand what changed and why without replaying the meeting that produced it.\",\n    sections: [\n      {\n        heading: \"Capture the durable parts\",\n        body: [\n          \"Attendance, discussion order, and every rejected sentence rarely matter later. The decision, the constraints behind it, and the consequences for future work usually do.\",\n        ],\n        points: [\n          \"Decision: what the team will do\",\n          \"Context: the constraint or evidence that shaped it\",\n          \"Consequences: what becomes easier, harder, or intentionally unsupported\",\n        ],\n      },\n      {\n        heading: \"Keep one source of truth\",\n        body: [\n          \"Link the note from the issue, pull request, or project page where the work continues. Copies in several tools drift quickly and make a settled decision look unresolved.\",\n        ],\n      },\n      {\n        heading: \"Update decisions explicitly\",\n        body: [\n          \"A decision can change. Add a short superseding note and link the two records instead of silently rewriting the old context. Future readers can then see both the current direction and the reason it moved.\",\n        ],\n      },\n    ],\n  },\n  {\n    slug: \"honest-forms\",\n    title: \"Shorter Forms Without Hidden Questions\",\n    summary: \"Use grouping, timing, and defaults to reduce friction while staying honest.\",\n    topic: \"Design\",\n    publishedAt: \"2026-06-02\",\n    publishedLabel: \"Jun 2\",\n    readTime: \"5 min\",\n    intro:\n      \"A form feels short when every question arrives at the right moment and its purpose is easy to understand.\",\n    sections: [\n      {\n        heading: \"Ask only when the answer matters\",\n        body: [\n          \"Collect information at the point where it changes the result. Questions needed only for a later workflow can wait until that workflow begins.\",\n        ],\n      },\n      {\n        heading: \"Use defaults without hiding choices\",\n        body: [\n          \"A default should reflect the most common safe answer and remain visible to the person submitting the form. Preselected values that are easy to miss save clicks by creating uncertainty elsewhere.\",\n        ],\n        points: [\n          \"Group fields by the task they support.\",\n          \"Explain why sensitive information is required.\",\n          \"Reveal conditional questions as soon as the triggering choice is made.\",\n        ],\n      },\n      {\n        heading: \"Audit the work after submit\",\n        body: [\n          \"Reducing visible fields is not an improvement if the user must correct assumptions on the next screen. Review the entire path through confirmation and editing, not only the initial form length.\",\n        ],\n      },\n    ],\n  },\n] as const satisfies readonly BlogPost[];\n\nexport function getBlogPost(slug: string) {\n  return BLOG_POSTS.find((post) => post.slug === slug);\n}\n",
      "type": "registry:lib",
      "target": "lib/blog-posts.ts"
    },
    {
      "path": "src/blocks/templates/blog-post-template.tsx",
      "content": "import { ArrowLeft, Clock3, FileText } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport type { BlogPost } from \"@/lib/blog-posts\";\n\nconst TEMPLATE_THEME =\n  \"[color-scheme:light] [--background:#fff5cc] [--secondary-background:#fff] [--foreground:#000] [--main-foreground:#000] [--main:#ffbe00] [--border:#000] [--ring:#000] [--box-shadow-x:0px] [--box-shadow-y:4px] [--reverse-box-shadow-x:0px] [--reverse-box-shadow-y:-4px] [--shadow:0px_4px_0px_0px_var(--border)] [--radius:10px] [--base-font-weight:500] [--heading-font-weight:700] dark:[color-scheme:dark] dark:[--background:#2c304c] dark:[--secondary-background:#222] dark:[--foreground:#ececec] dark:[--main:#ffbe00] dark:[--ring:#fff]\";\n\ntype BlogPostTemplateProps = {\n  backHref?: string;\n  post: BlogPost;\n};\n\nexport default function BlogPostTemplate({ backHref = \"/blog\", post }: BlogPostTemplateProps) {\n  return (\n    <div\n      id=\"top\"\n      className={`flex min-h-dvh flex-col bg-background text-foreground ${TEMPLATE_THEME}`}\n    >\n      <header className=\"border-b-2 border-border bg-secondary-background\">\n        <div className=\"mx-auto flex h-12 w-full max-w-3xl items-center justify-between px-4 sm:px-6\">\n          <a\n            href={backHref}\n            className=\"group flex items-center gap-2 font-heading\"\n            aria-label=\"Worklog home\"\n          >\n            <span className=\"grid size-7 place-items-center rounded-base border-2 border-border bg-main text-main-foreground shadow-shadow transition-[translate,box-shadow] duration-[140ms] ease-[ease] motion-reduce:transition-none group-hover:translate-x-pressX group-hover:translate-y-pressY group-hover:shadow-press group-focus-visible:translate-x-pressX group-focus-visible:translate-y-pressY group-focus-visible:shadow-press group-active:translate-x-boxShadowX group-active:translate-y-boxShadowY group-active:shadow-[0_0_0_0_var(--border)]\">\n              <FileText aria-hidden=\"true\" className=\"size-3.5\" />\n            </span>\n            <span className=\"text-sm\">WORKLOG</span>\n          </a>\n\n          <a className=\"text-xs font-heading underline-offset-4 hover:underline\" href={backHref}>\n            All posts\n          </a>\n        </div>\n      </header>\n\n      <main className=\"mx-auto w-full max-w-3xl flex-1 px-4 py-8 sm:px-6 sm:py-12\">\n        <Button asChild nativeButton={false} size=\"sm\" variant=\"neutral\">\n          <a href={backHref}>\n            <ArrowLeft aria-hidden=\"true\" />\n            All posts\n          </a>\n        </Button>\n\n        <article className=\"mt-8\">\n          <header>\n            <p className=\"text-sm font-heading text-foreground/65\">{post.topic}</p>\n            <h1 className=\"mt-3 max-w-2xl text-4xl leading-tight font-heading sm:text-5xl\">\n              {post.title}\n            </h1>\n            <p className=\"mt-5 max-w-2xl text-lg leading-8 text-foreground/75\">{post.summary}</p>\n            <div className=\"mt-5 flex flex-wrap items-center gap-3 text-sm font-heading text-foreground/60\">\n              <time dateTime={post.publishedAt}>{post.publishedLabel}, 2026</time>\n              <span aria-hidden=\"true\">/</span>\n              <span className=\"flex items-center gap-1.5\">\n                <Clock3 aria-hidden=\"true\" className=\"size-4\" />\n                {post.readTime} read\n              </span>\n            </div>\n          </header>\n\n          <div className=\"mt-8 border-y-2 border-border bg-secondary-background px-5 py-6 text-lg leading-8 sm:px-7\">\n            <p>{post.intro}</p>\n          </div>\n\n          <div className=\"mt-9 space-y-10\">\n            {post.sections.map((section, index) => {\n              const headingId = `${post.slug}-section-${index + 1}`;\n\n              return (\n                <section key={section.heading} aria-labelledby={headingId}>\n                  <h2 id={headingId} className=\"text-2xl leading-tight font-heading sm:text-3xl\">\n                    {section.heading}\n                  </h2>\n                  <div className=\"mt-4 space-y-4 text-base leading-8 text-foreground/80 sm:text-lg\">\n                    {section.body.map((paragraph) => (\n                      <p key={paragraph}>{paragraph}</p>\n                    ))}\n                  </div>\n                  {section.points ? (\n                    <ul className=\"mt-5 list-square space-y-2 pl-6 leading-7 text-foreground/80\">\n                      {section.points.map((point) => (\n                        <li key={point}>{point}</li>\n                      ))}\n                    </ul>\n                  ) : null}\n                </section>\n              );\n            })}\n          </div>\n        </article>\n      </main>\n\n      <footer className=\"border-t-2 border-border bg-secondary-background\">\n        <div className=\"mx-auto flex w-full max-w-3xl items-center justify-between gap-4 px-4 py-4 text-xs sm:px-6\">\n          <p className=\"font-heading\">WORKLOG / 2026</p>\n          <a href=\"#top\" className=\"font-heading underline-offset-4 hover:underline\">\n            Back to top\n          </a>\n        </div>\n      </footer>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/templates/blog-post-template.tsx"
    },
    {
      "path": "src/blocks/templates/blog-template.tsx",
      "content": "\"use client\";\n\nimport { ArrowUpRight, Clock3, FileText, Search, X } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { BLOG_POSTS } from \"@/lib/blog-posts\";\n\nconst TEMPLATE_THEME =\n  \"[color-scheme:light] [--background:#fff5cc] [--secondary-background:#fff] [--foreground:#000] [--main-foreground:#000] [--main:#ffbe00] [--border:#000] [--ring:#000] [--box-shadow-x:0px] [--box-shadow-y:4px] [--reverse-box-shadow-x:0px] [--reverse-box-shadow-y:-4px] [--shadow:0px_4px_0px_0px_var(--border)] [--radius:10px] [--base-font-weight:500] [--heading-font-weight:700] dark:[color-scheme:dark] dark:[--background:#2c304c] dark:[--secondary-background:#222] dark:[--foreground:#ececec] dark:[--main:#ffbe00] dark:[--ring:#fff]\";\n\ntype BlogTemplateProps = {\n  basePath?: string;\n};\n\nfunction getPostHref(basePath: string, slug: string) {\n  return `${basePath.replace(/\\/$/, \"\")}/${slug}`;\n}\n\nexport default function BlogTemplate({ basePath = \"/blog\" }: BlogTemplateProps) {\n  const [query, setQuery] = useState(\"\");\n\n  const filteredPosts = useMemo(() => {\n    const normalizedQuery = query.trim().toLowerCase();\n\n    if (!normalizedQuery) return BLOG_POSTS;\n\n    return BLOG_POSTS.filter((post) =>\n      [post.title, post.summary, post.topic].join(\" \").toLowerCase().includes(normalizedQuery),\n    );\n  }, [query]);\n\n  return (\n    <div\n      id=\"top\"\n      className={`flex min-h-dvh flex-col bg-background text-foreground ${TEMPLATE_THEME}`}\n    >\n      <header className=\"border-b-2 border-border bg-secondary-background\">\n        <div className=\"mx-auto flex h-12 max-w-3xl items-center justify-between px-4 sm:px-6\">\n          <a\n            href=\"#top\"\n            className=\"group flex items-center gap-2 font-heading\"\n            aria-label=\"Worklog home\"\n          >\n            <span className=\"grid size-7 place-items-center rounded-base border-2 border-border bg-main text-main-foreground shadow-shadow transition-[translate,box-shadow] duration-[140ms] ease-[ease] motion-reduce:transition-none group-hover:translate-x-pressX group-hover:translate-y-pressY group-hover:shadow-press group-focus-visible:translate-x-pressX group-focus-visible:translate-y-pressY group-focus-visible:shadow-press group-active:translate-x-boxShadowX group-active:translate-y-boxShadowY group-active:shadow-[0_0_0_0_var(--border)]\">\n              <FileText aria-hidden=\"true\" className=\"size-3.5\" />\n            </span>\n            <span className=\"text-sm\">WORKLOG</span>\n          </a>\n\n          <nav\n            className=\"flex items-center gap-3 text-xs font-heading\"\n            aria-label=\"Primary navigation\"\n          >\n            <a className=\"underline-offset-4 hover:underline\" href=\"#posts\">\n              Posts\n            </a>\n            <a className=\"underline-offset-4 hover:underline\" href=\"#post-search\">\n              Search\n            </a>\n          </nav>\n        </div>\n      </header>\n\n      <main className=\"mx-auto w-full max-w-3xl flex-1 px-4 py-8 sm:px-6 sm:py-10\">\n        <section aria-labelledby=\"page-title\">\n          <h1 id=\"page-title\" className=\"text-3xl leading-tight font-heading sm:text-4xl\">\n            Latest posts\n          </h1>\n\n          <search className=\"mt-4\">\n            <label className=\"sr-only\" htmlFor=\"post-search\">\n              Search posts\n            </label>\n            <div className=\"relative\">\n              <Search\n                aria-hidden=\"true\"\n                className=\"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2\"\n              />\n              <Input\n                id=\"post-search\"\n                type=\"search\"\n                value={query}\n                onChange={(event) => setQuery(event.target.value)}\n                placeholder=\"Search posts...\"\n                className=\"h-11 bg-secondary-background pr-11 pl-10\"\n              />\n              {query ? (\n                <Button\n                  type=\"button\"\n                  size=\"icon-xs\"\n                  variant=\"ghost\"\n                  className=\"absolute top-1/2 right-2 -translate-y-1/2\"\n                  onClick={() => setQuery(\"\")}\n                  aria-label=\"Clear search\"\n                  title=\"Clear search\"\n                >\n                  <X aria-hidden=\"true\" />\n                </Button>\n              ) : null}\n            </div>\n            <p className=\"sr-only\" aria-live=\"polite\">\n              {query\n                ? `${filteredPosts.length} ${filteredPosts.length === 1 ? \"result\" : \"results\"}`\n                : \"\"}\n            </p>\n          </search>\n        </section>\n\n        <section id=\"posts\" className=\"mt-4 scroll-mt-4\" aria-label=\"Post archive\">\n          {filteredPosts.length > 0 ? (\n            <ol className=\"overflow-hidden rounded-base border-2 border-border bg-secondary-background shadow-shadow\">\n              {filteredPosts.map((post) => (\n                <li key={post.slug} className=\"border-b-2 border-border last:border-b-0\">\n                  <article className=\"grid grid-cols-[minmax(0,1fr)_2.25rem] gap-x-3 gap-y-2 p-4 sm:grid-cols-[6rem_minmax(0,1fr)_2.25rem] sm:items-center sm:gap-4 sm:p-5\">\n                    <time\n                      className=\"col-span-2 text-xs font-heading text-foreground/60 sm:col-span-1\"\n                      dateTime={post.publishedAt}\n                    >\n                      {post.publishedLabel}\n                    </time>\n\n                    <div className=\"min-w-0\">\n                      <h2 className=\"text-lg leading-snug font-heading sm:text-xl\">\n                        <a\n                          className=\"underline-offset-4 hover:underline\"\n                          href={getPostHref(basePath, post.slug)}\n                        >\n                          {post.title}\n                        </a>\n                      </h2>\n                      <p className=\"mt-1 text-sm leading-5 text-foreground/70\">{post.summary}</p>\n                      <p className=\"mt-2 flex items-center gap-3 text-xs font-heading text-foreground/60\">\n                        <span>{post.topic}</span>\n                        <span className=\"flex items-center gap-1\">\n                          <Clock3 aria-hidden=\"true\" className=\"size-3.5\" />\n                          {post.readTime}\n                        </span>\n                      </p>\n                    </div>\n\n                    <Button\n                      asChild\n                      nativeButton={false}\n                      size=\"icon-sm\"\n                      variant=\"ghost\"\n                      className=\"self-center\"\n                    >\n                      <a\n                        href={getPostHref(basePath, post.slug)}\n                        aria-label={`Read ${post.title}`}\n                        title={`Read ${post.title}`}\n                      >\n                        <ArrowUpRight aria-hidden=\"true\" />\n                      </a>\n                    </Button>\n                  </article>\n                </li>\n              ))}\n            </ol>\n          ) : (\n            <p className=\"py-10 text-center text-sm text-foreground/60\">No posts found.</p>\n          )}\n        </section>\n      </main>\n\n      <footer className=\"border-t-2 border-border bg-secondary-background\">\n        <div className=\"mx-auto max-w-3xl px-4 py-4 text-xs sm:px-6\">\n          <p className=\"font-heading\">WORKLOG / 2026</p>\n        </div>\n      </footer>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/templates/blog-template.tsx"
    },
    {
      "path": "src/blocks/templates/pages/blog/page.tsx",
      "content": "import BlogTemplate from \"@/components/templates/blog-template\";\n\nexport default function Page() {\n  return <BlogTemplate />;\n}\n",
      "type": "registry:page",
      "target": "app/blog/page.tsx"
    },
    {
      "path": "src/blocks/templates/pages/blog/[slug]/page.tsx",
      "content": "import type { Metadata } from \"next\";\nimport { notFound } from \"next/navigation\";\n\nimport BlogPostTemplate from \"@/components/templates/blog-post-template\";\nimport { BLOG_POSTS, getBlogPost } from \"@/lib/blog-posts\";\n\ntype BlogPostPageProps = {\n  params: Promise<{ slug: string }>;\n};\n\nexport function generateStaticParams() {\n  return BLOG_POSTS.map(({ slug }) => ({ slug }));\n}\n\nexport async function generateMetadata({ params }: BlogPostPageProps): Promise<Metadata> {\n  const { slug } = await params;\n  const post = getBlogPost(slug);\n\n  if (!post) return {};\n\n  return {\n    title: post.title,\n    description: post.summary,\n  };\n}\n\nexport default async function BlogPostPage({ params }: BlogPostPageProps) {\n  const { slug } = await params;\n  const post = getBlogPost(slug);\n\n  if (!post) notFound();\n\n  return <BlogPostTemplate post={post} />;\n}\n",
      "type": "registry:page",
      "target": "app/blog/[slug]/page.tsx"
    }
  ],
  "categories": [
    "template",
    "blog"
  ],
  "type": "registry:block"
}