{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "recipe-1",
  "title": "Recipe 1: scale, check off and time",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A recipe you can cook from: change how many it serves and every amount scales the way a cook reads it, switch metric and US units, tick off ingredients and steps, and start a timer on the steps that need waiting.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/cover-art.json",
    "https://ui.ballmac.com/r/segmented-control.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/recipe-1/recipe-1.tsx",
      "content": "// Ballmac UI: Recipe 1. https://ui.ballmac.com/blocks/recipe-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { BellRing, ChefHat, Clock, Minus, Pause, Play, Plus, RotateCcw, Users } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { CoverArt } from \"@/components/ballmac/cover-art\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { cn } from \"@/lib/utils\"\n\ntype Recipe1Ingredient = { id: string; /** Amount for the recipe's own serving count. */ qty: number; /** g, ml, tbsp, tsp, cup, or empty for countable things. */ unit?: string; name: string; note?: string }\ntype Recipe1Step = { id: string; text: string; /** If the step needs waiting, how many minutes. */ minutes?: number }\n\ntype Recipe1Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  title?: string\n  intro?: string\n  /** How many people the amounts are written for. */\n  serves?: number\n  prep?: number\n  cook?: number\n  difficulty?: string\n  ingredients?: Recipe1Ingredient[]\n  steps?: Recipe1Step[]\n  /** Kcal per serving. */\n  calories?: number\n  /** Milliseconds a timer second takes. Leave at 1000; tests make it small. */\n  tick?: number\n}\n\nconst ing0: Recipe1Ingredient[] = [\n  { id: \"i1\", qty: 400, unit: \"g\", name: \"spaghetti\" },\n  { id: \"i2\", qty: 150, unit: \"g\", name: \"guanciale or pancetta\", note: \"cut in strips\" },\n  { id: \"i3\", qty: 4, name: \"egg yolks\", note: \"plus 1 whole egg\" },\n  { id: \"i4\", qty: 80, unit: \"g\", name: \"pecorino, finely grated\" },\n  { id: \"i5\", qty: 2, unit: \"tsp\", name: \"black pepper\", note: \"freshly cracked\" },\n  { id: \"i6\", qty: 1, unit: \"tbsp\", name: \"olive oil\" },\n  { id: \"i7\", qty: 200, unit: \"ml\", name: \"pasta water\", note: \"kept back\" },\n]\nconst steps0: Recipe1Step[] = [\n  { id: \"s1\", text: \"Bring a large pot of well-salted water to the boil. Meanwhile, whisk the yolks, the whole egg, most of the pecorino and the pepper in a bowl.\" },\n  { id: \"s2\", text: \"Fry the guanciale in the oil over a medium heat until the fat has melted and the edges are crisp.\", minutes: 8 },\n  { id: \"s3\", text: \"Cook the spaghetti until just short of done. Scoop out a cup of the pasta water before you drain it.\", minutes: 10 },\n  { id: \"s4\", text: \"Take the pan off the heat. Toss the pasta in the fat, then add the egg mixture with a splash of the water, stirring fast so it turns glossy rather than scrambled.\" },\n  { id: \"s5\", text: \"Loosen with more water if it needs it, then serve at once with the rest of the cheese and more pepper.\" },\n]\n\nconst FR: [number, string][] = [[0.125, \"⅛\"], [0.25, \"¼\"], [1 / 3, \"⅓\"], [0.5, \"½\"], [2 / 3, \"⅔\"], [0.75, \"¾\"]]\n// Amounts read the way a cook says them: halves and quarters for spoons and cups, whole grams for weights.\nfunction show(qty: number, unit: string | undefined) {\n  if (unit === \"g\" || unit === \"ml\") return qty >= 100 ? String(Math.round(qty / 5) * 5) : String(Math.round(qty))\n  const whole = Math.floor(qty + 1e-6)\n  const rest = qty - whole\n  if (rest < 0.06) return String(whole || (qty > 0 ? \"⅛\" : \"0\"))\n  const near = FR.reduce((a, b) => (Math.abs(b[0] - rest) < Math.abs(a[0] - rest) ? b : a))\n  if (Math.abs(near[0] - rest) > 0.07) return String(Math.round(qty * 10) / 10)\n  return `${whole ? whole + \" \" : \"\"}${near[1]}`\n}\nconst toUs = (qty: number, unit: string | undefined): [number, string | undefined] => (unit === \"g\" ? [qty / 28.35, \"oz\"] : unit === \"ml\" ? [qty / 29.57, \"fl oz\"] : [qty, unit])\nconst clock = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, \"0\")}`\n\nfunction Timer({ minutes, label, tick }: { minutes: number; label: string; tick: number }) {\n  const total = minutes * 60\n  const [left, setLeft] = React.useState(total)\n  const [running, setRunning] = React.useState(false)\n  React.useEffect(() => {\n    if (!running) return\n    const t = setInterval(() => setLeft((l) => Math.max(0, l - 1)), tick)\n    return () => clearInterval(t)\n  }, [running, tick])\n  const done = left === 0\n  React.useEffect(() => {\n    if (done) setRunning(false)\n  }, [done])\n  return (\n    <div className={cn(\"mt-3 flex flex-wrap items-center gap-3 rounded-xl border p-3\", done ? \"border-chart-2/40 bg-chart-2/10\" : running && \"border-foreground/30\")}>\n      <span aria-hidden=\"true\" className=\"relative size-10 shrink-0\">\n        <svg viewBox=\"0 0 36 36\" className=\"size-10 -rotate-90 rtl:rotate-90\">\n          <circle cx=\"18\" cy=\"18\" r=\"15\" fill=\"none\" strokeWidth=\"3\" className=\"stroke-muted\" />\n          <circle cx=\"18\" cy=\"18\" r=\"15\" fill=\"none\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeDasharray={2 * Math.PI * 15} strokeDashoffset={2 * Math.PI * 15 * (1 - left / total)} className=\"stroke-foreground\" />\n        </svg>\n      </span>\n      <p className=\"min-w-16 text-2xl font-semibold tabular-nums\" role=\"timer\" aria-label={`${label}: ${Math.floor(left / 60)} minutes ${left % 60} seconds left`}>\n        {clock(left)}\n      </p>\n      <div className=\"ms-auto flex flex-wrap gap-1.5\">\n        {!done ? (\n          <Button size=\"sm\" variant=\"outline\" onClick={() => setRunning(!running)} aria-pressed={running}>\n            {running ? <Pause aria-hidden=\"true\" /> : <Play aria-hidden=\"true\" />}\n            {running ? \"Pause\" : left === total ? `Start ${minutes} min timer` : \"Resume\"}\n          </Button>\n        ) : (\n          <p className=\"flex items-center gap-2 text-sm font-semibold\">\n            <BellRing aria-hidden=\"true\" className=\"size-4\" />\n            Time is up\n          </p>\n        )}\n        <Button size=\"icon\" variant=\"ghost\" aria-label=\"Reset the timer\" onClick={() => { setRunning(false); setLeft(total) }}>\n          <RotateCcw aria-hidden=\"true\" />\n        </Button>\n      </div>\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {done ? `${label}: time is up` : \"\"}\n      </p>\n    </div>\n  )\n}\n\n/**\n * A recipe you can cook from: change how many people it serves and every amount scales (read as a cook would, ½ and ¼, whole grams), switch between metric and US\n * units, tick off ingredients as you gather them and steps as you finish them, and start a timer right on the steps that need waiting. Progress is counted for both lists.\n */\nfunction Recipe1({ title = \"Spaghetti carbonara\", intro = \"Four ingredients, one pan and ten minutes of attention. The trick is taking the pan off the heat before the eggs go in.\", serves = 4, prep = 10, cook = 20, difficulty = \"Easy\", ingredients = ing0, steps = steps0, calories = 640, tick = 1000, className, ...props }: Recipe1Props) {\n  const id = React.useId()\n  const [people, setPeople] = React.useState(serves)\n  const [units, setUnits] = React.useState<\"metric\" | \"us\">(\"metric\")\n  const [have, setHave] = React.useState<Set<string>>(new Set())\n  const [done, setDone] = React.useState<Set<string>>(new Set())\n  const factor = people / serves\n  const flip = (set: Set<string>, k: string) => { const n = new Set(set); if (n.has(k)) n.delete(k); else n.add(k); return n }\n  const next = steps.find((s) => !done.has(s.id))\n\n  return (\n    <section data-slot=\"recipe-1\" aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-5xl px-4 py-10 sm:px-6\", className)} {...props}>\n      <div className=\"relative isolate overflow-hidden rounded-3xl border\">\n        <CoverArt seed={title} variant=\"waves\" className=\"absolute inset-0 -z-20 scale-110 saturate-150\" />\n        <div aria-hidden=\"true\" className=\"from-background via-background/85 to-background/30 absolute inset-0 -z-10 bg-gradient-to-r rtl:bg-gradient-to-l\" />\n        <div className=\"p-6 sm:p-10\">\n          <p className=\"text-muted-foreground flex items-center gap-2 text-sm font-medium\">\n            <ChefHat aria-hidden=\"true\" className=\"size-4\" />\n            {difficulty}\n          </p>\n          <h1 id={`${id}-title`} className=\"mt-3 max-w-xl text-4xl font-semibold tracking-[-0.045em] text-balance sm:text-6xl\">\n            {title}\n          </h1>\n          <p className=\"text-muted-foreground mt-4 max-w-xl text-pretty sm:text-lg\">{intro}</p>\n          <dl className=\"mt-6 flex flex-wrap gap-x-8 gap-y-3 text-sm\">\n            {[[\"Prep\", `${prep} min`], [\"Cook\", `${cook} min`], [\"Total\", `${prep + cook} min`], [\"Energy\", `${calories} kcal each`]].map(([k, v]) => (\n              <div key={k}>\n                <dt className=\"text-muted-foreground text-xs\">{k}</dt>\n                <dd className=\"font-semibold tabular-nums\">{v}</dd>\n              </div>\n            ))}\n          </dl>\n        </div>\n      </div>\n\n      <div className=\"mt-8 grid grid-cols-1 items-start gap-8 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)]\">\n        <div className=\"bg-card rounded-3xl border p-5 sm:p-7 lg:sticky lg:top-6\">\n          <div className=\"flex flex-wrap items-center justify-between gap-3\">\n            <h2 className=\"text-xl font-semibold tracking-tight\">Ingredients</h2>\n            <SegmentedControl value={units} onValueChange={(v) => setUnits(v as \"metric\" | \"us\")} aria-label=\"Units\" size=\"sm\">\n              <SegmentedControlItem value=\"metric\">Metric</SegmentedControlItem>\n              <SegmentedControlItem value=\"us\">US</SegmentedControlItem>\n            </SegmentedControl>\n          </div>\n          <div className=\"bg-muted/50 mt-4 flex items-center justify-between gap-3 rounded-2xl p-3\">\n            <p className=\"flex items-center gap-2 text-sm font-medium\">\n              <Users aria-hidden=\"true\" className=\"size-4\" />\n              <span id={`${id}-serves`}>Serves</span>\n            </p>\n            <div className=\"flex items-center gap-1\" role=\"group\" aria-labelledby={`${id}-serves`}>\n              <Button size=\"icon\" variant=\"outline\" aria-label=\"Fewer people\" disabled={people <= 1} onClick={() => setPeople(people - 1)}>\n                <Minus aria-hidden=\"true\" />\n              </Button>\n              <output className=\"w-10 text-center text-xl font-semibold tabular-nums\" aria-live=\"polite\" aria-label={`${people} people`}>\n                {people}\n              </output>\n              <Button size=\"icon\" variant=\"outline\" aria-label=\"More people\" disabled={people >= 24} onClick={() => setPeople(people + 1)}>\n                <Plus aria-hidden=\"true\" />\n              </Button>\n            </div>\n          </div>\n          <ul className=\"mt-4 divide-y\">\n            {ingredients.map((g) => {\n              const [q, u] = units === \"us\" ? toUs(g.qty * factor, g.unit) : [g.qty * factor, g.unit]\n              const on = have.has(g.id)\n              return (\n                <li key={g.id}>\n                  <label className=\"flex cursor-pointer items-start gap-3 py-3\">\n                    <input type=\"checkbox\" checked={on} onChange={() => setHave(flip(have, g.id))} className=\"accent-foreground mt-1 size-4\" />\n                    <span className={cn(\"min-w-0 flex-1\", on && \"text-muted-foreground line-through\")}>\n                      <span className=\"font-semibold tabular-nums\">\n                        {units === \"us\" && (g.unit === \"g\" || g.unit === \"ml\") ? (Math.round(q * 10) / 10).toString() : show(q, u)} {u}\n                      </span>{\" \"}\n                      {g.name}\n                      {g.note ? <span className=\"text-muted-foreground text-sm\"> ({g.note})</span> : null}\n                    </span>\n                  </label>\n                </li>\n              )\n            })}\n          </ul>\n          <p className=\"text-muted-foreground mt-2 text-sm tabular-nums\" aria-live=\"polite\">\n            {have.size} of {ingredients.length} gathered\n          </p>\n        </div>\n\n        <div>\n          <h2 className=\"text-xl font-semibold tracking-tight\">Method</h2>\n          <ol className=\"mt-4 grid grid-cols-1 gap-4\">\n            {steps.map((s, i) => {\n              const finished = done.has(s.id)\n              const current = next?.id === s.id\n              return (\n                <li key={s.id} className={cn(\"bg-card min-w-0 rounded-2xl border p-4 transition-colors sm:p-5 motion-reduce:transition-none\", current && \"border-foreground ring-foreground ring-1\", finished && \"bg-muted/40\")}>\n                  <div className=\"flex gap-4\">\n                    <span aria-hidden=\"true\" className={cn(\"flex size-9 shrink-0 items-center justify-center rounded-full border text-sm font-semibold tabular-nums\", finished && \"bg-foreground text-background border-foreground\")}>\n                      {finished ? \"✓\" : i + 1}\n                    </span>\n                    <div className=\"min-w-0 flex-1\">\n                      <p className={cn(\"leading-relaxed text-pretty\", finished && \"text-muted-foreground\")}>\n                        <span className=\"sr-only\">Step {i + 1}. </span>\n                        {s.text}\n                      </p>\n                      {s.minutes ? <Timer minutes={s.minutes} label={`Step ${i + 1}`} tick={tick} /> : null}\n                      <label className=\"mt-3 flex w-fit cursor-pointer items-center gap-2 text-sm\">\n                        <input type=\"checkbox\" checked={finished} onChange={() => setDone(flip(done, s.id))} className=\"accent-foreground size-4\" />\n                        Done <span className=\"sr-only\">with step {i + 1}</span>\n                      </label>\n                    </div>\n                  </div>\n                </li>\n              )\n            })}\n          </ol>\n          <p className=\"text-muted-foreground mt-4 flex items-center gap-2 text-sm tabular-nums\" aria-live=\"polite\">\n            <Clock aria-hidden=\"true\" className=\"size-4\" />\n            {done.size === steps.length ? \"All done. Enjoy.\" : `${done.size} of ${steps.length} steps done`}\n          </p>\n        </div>\n      </div>\n    </section>\n  )\n}\n\nexport { Recipe1, type Recipe1Props, type Recipe1Ingredient, type Recipe1Step }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/recipe-1/recipe-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "recipe",
      "cooking",
      "ingredients",
      "servings",
      "timer",
      "food",
      "kitchen",
      "method",
      "meal",
      "baking"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "A recipe. <Recipe1 title intro serves={4} prep={10} cook={20} difficulty calories ingredients={[{ id, qty, unit: 'g' | 'ml' | 'tbsp' | 'tsp' | 'cup' | undefined, name, note }]} steps={[{ id, text, minutes }]} />. qty is for the recipe's own serves; changing the servings scales every amount. Spoons and cups read as ½ ¼ ⅓ fractions, g and ml as whole numbers (rounded to 5 above 100); US units turn g into oz and ml into fl oz. A step with minutes gets a start, pause and reset timer that says 'Time is up'.",
      "whenToUse": [
        "Food blogs, meal-kit sites and cooking apps",
        "Any how-to with amounts that scale and steps that need waiting"
      ],
      "whenNotToUse": [
        "A shopping list across recipes",
        "A meal planner calendar"
      ],
      "composesWith": [
        "gallery-1",
        "reviews-1",
        "comments-1",
        "header-1"
      ],
      "a11y": [
        {
          "keys": "Tab, Space",
          "action": "Servings are Fewer and More buttons with the count read as '4 people'; every ingredient and step has a real checkbox, and the counts (3 of 7 gathered) are announced politely"
        },
        {
          "keys": "Timer",
          "action": "Each timer is a role=timer that reads '8 minutes 0 seconds left', its button says Start, Pause or Resume, and 'Time is up' is announced"
        },
        {
          "keys": "Steps",
          "action": "Each step reads 'Step 2.' first, and the current one is outlined as well as numbered"
        }
      ],
      "customization": [
        "serves is the base the amounts are written for",
        "Countable ingredients (eggs) round to a sensible fraction or one decimal",
        "The timer ring and the hero mirror in right-to-left"
      ]
    },
    "statesNone": "Static content with its own tools: nothing to load, submit or fail.",
    "examples": [
      "recipe-1-demo",
      "recipe-1-baking"
    ],
    "url": "https://ui.ballmac.com/components/recipe-1"
  },
  "categories": [
    "blocks",
    "recipe"
  ],
  "type": "registry:block"
}