{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tasks-1",
  "title": "Tasks 1: task list",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A tasks page: a progress ring and quick add, tasks grouped as overdue, today, upcoming and no date, a strike-through that draws itself, inline edit, reordering by button or Alt and arrow keys, shortcuts and Undo on delete.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/blur-fade.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/checkbox.json",
    "https://ui.ballmac.com/r/circular-progress.json",
    "https://ui.ballmac.com/r/kbd.json",
    "https://ui.ballmac.com/r/segmented-control.json",
    "https://ui.ballmac.com/r/select.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/tasks-1/tasks-1.tsx",
      "content": "// Ballmac UI: Tasks 1. https://ui.ballmac.com/blocks/tasks-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { CalendarDays, Check, ChevronDown, ChevronUp, CircleCheckBig, Flag, ListTodo, Pencil, Plus, Trash2, Undo2 } from \"lucide-react\"\n\nimport { BlurFade } from \"@/components/ballmac/blur-fade\"\nimport { Button } from \"@/components/ballmac/button\"\nimport { Checkbox } from \"@/components/ballmac/checkbox\"\nimport { CircularProgress } from \"@/components/ballmac/circular-progress\"\nimport { Kbd } from \"@/components/ballmac/kbd\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ballmac/select\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Tasks1Task = {\n  id: string\n  title: string\n  done?: boolean\n  /** A date as YYYY-MM-DD. */\n  due?: string\n  priority?: \"normal\" | \"high\"\n  /** Initials of who it is for. */\n  assignee?: string\n  /** Short labels, such as \"Design\". */\n  labels?: string[]\n}\n\ntype Tasks1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    tasks?: Tasks1Task[]\n    title?: string\n    /** Today's date as YYYY-MM-DD. Pass it in so server and browser agree; it decides what is overdue. */\n    today?: string\n    /** BCP 47 locale for dates. */\n    locale?: string\n    /** Called with the new task. */\n    onAdd?: (task: Tasks1Task) => void\n    /** Called when a task is completed or reopened. */\n    onToggle?: (id: string, done: boolean) => void\n    /** Called when a title is changed. */\n    onEdit?: (id: string, title: string) => void\n    /** Called straight away when a task is deleted. The block offers Undo for a few seconds. */\n    onDelete?: (id: string) => void\n    /** Called when Undo is pressed, with the task to put back. */\n    onRestore?: (task: Tasks1Task) => void\n    /** Called after a task is moved, with the new order of ids. */\n    onReorder?: (ids: string[]) => void\n    /** Show the completed tasks open at first. */\n    defaultShowCompleted?: boolean\n  }\n\nconst sample: Tasks1Task[] = [\n  { id: \"t1\", title: \"Send the Q4 plan to Priya\", due: \"2026-10-16\", priority: \"high\", assignee: \"JL\", labels: [\"Planning\"] },\n  { id: \"t2\", title: \"Review the release notes for 2.5\", due: \"2026-10-18\", priority: \"high\", assignee: \"JL\", labels: [\"Release\"] },\n  { id: \"t3\", title: \"Reply to the pull request on Gridwork\", due: \"2026-10-18\", assignee: \"JL\", labels: [\"Code\"] },\n  { id: \"t4\", title: \"Book the venue for the autumn offsite\", due: \"2026-10-19\", assignee: \"MA\", labels: [\"Team\"] },\n  { id: \"t5\", title: \"Update the copy on the pricing page\", due: \"2026-10-21\", assignee: \"AK\", labels: [\"Website\"] },\n  { id: \"t6\", title: \"Write the launch announcement\", due: \"2026-10-23\", priority: \"high\", assignee: \"JL\", labels: [\"Release\", \"Writing\"] },\n  { id: \"t7\", title: \"Tidy up the icon set\", labels: [\"Design\"] },\n  { id: \"t8\", title: \"Plan first-quarter hiring\", assignee: \"JL\", labels: [\"Planning\"] },\n  { id: \"t9\", title: \"Fix the webhook retry bug\", done: true, due: \"2026-10-17\", assignee: \"TN\", labels: [\"Code\"] },\n  { id: \"t10\", title: \"Order lunch for the team\", done: true, due: \"2026-10-17\", assignee: \"MA\" },\n  { id: \"t11\", title: \"Rotate the staging key\", done: true, assignee: \"JL\", labels: [\"Security\"] },\n]\n\nconst DAY = 86_400_000\nconst toTime = (iso: string) => Date.UTC(+iso.slice(0, 4), +iso.slice(5, 7) - 1, +iso.slice(8, 10))\nconst addDays = (iso: string, n: number) => new Date(toTime(iso) + n * DAY).toISOString().slice(0, 10)\n\nfunction TasksSkeleton() {\n  return (\n    <div className=\"grid gap-3\" aria-hidden=\"true\">\n      <Skeleton className=\"h-28 rounded-2xl\" />\n      {[0, 1, 2, 3, 4].map((i) => (\n        <div key={i} className=\"flex items-center gap-4 rounded-xl border p-4\">\n          <Skeleton className=\"size-5 rounded-md\" />\n          <Skeleton className=\"h-4 flex-1\" style={{ maxWidth: `${70 - i * 8}%` }} />\n          <Skeleton className=\"h-5 w-16 rounded-full\" />\n        </div>\n      ))}\n    </div>\n  )\n}\n\n/**\n * A tasks page: a progress ring and quick add, tasks grouped as overdue, today, upcoming and no date with the due date written\n * plainly, a strike-through that draws itself when you complete one, inline editing, priority with a word and a flag, reordering by\n * button or Alt and the arrow keys, keyboard shortcuts, and an Undo bar after deleting.\n */\nfunction Tasks1({\n  tasks: tasksProp = sample,\n  title = \"My tasks\",\n  today = \"2026-10-18\",\n  locale = \"en-US\",\n  onAdd,\n  onToggle,\n  onEdit,\n  onDelete,\n  onRestore,\n  onReorder,\n  defaultShowCompleted = false,\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Tasks1Props) {\n  const id = React.useId()\n  const addRef = React.useRef<HTMLInputElement>(null)\n  const listRef = React.useRef<HTMLDivElement>(null)\n  const counter = React.useRef(0)\n  const [tasks, setTasks] = React.useState(tasksProp)\n  React.useEffect(() => setTasks(tasksProp), [tasksProp])\n  const [draft, setDraft] = React.useState(\"\")\n  const [dueChoice, setDueChoice] = React.useState<\"today\" | \"tomorrow\" | \"week\" | \"none\">(\"today\")\n  const [high, setHigh] = React.useState(false)\n  const [filter, setFilter] = React.useState<\"all\" | \"high\">(\"all\")\n  const [showDone, setShowDone] = React.useState(defaultShowCompleted)\n  const [editing, setEditing] = React.useState<string | null>(null)\n  const [editText, setEditText] = React.useState(\"\")\n  const [undo, setUndo] = React.useState<{ task: Tasks1Task; index: number } | null>(null)\n  const [announce, setAnnounce] = React.useState(\"\")\n  const ready = state === \"ready\" || state === \"empty\"\n  const all = state === \"empty\" ? [] : tasks\n\n  React.useEffect(() => {\n    if (!undo) return\n    const t = setTimeout(() => setUndo(null), 7000)\n    return () => clearTimeout(t)\n  }, [undo])\n  // \"n\" jumps to the new task box from anywhere, unless you are typing\n  React.useEffect(() => {\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key !== \"n\" || e.metaKey || e.ctrlKey || e.altKey) return\n      const el = e.target as HTMLElement | null\n      if (el && (el.tagName === \"INPUT\" || el.tagName === \"TEXTAREA\" || el.tagName === \"SELECT\" || el.isContentEditable)) return\n      e.preventDefault()\n      addRef.current?.focus()\n    }\n    document.addEventListener(\"keydown\", onKey)\n    return () => document.removeEventListener(\"keydown\", onKey)\n  }, [])\n\n  const dateFmt = new Intl.DateTimeFormat(locale, { weekday: \"short\", day: \"numeric\", month: \"short\", timeZone: \"UTC\" })\n  function dueLabel(iso: string, done?: boolean): { text: string; tone: \"overdue\" | \"today\" | \"normal\" } {\n    const diff = Math.round((toTime(iso) - toTime(today)) / DAY)\n    if (done) return { text: dateFmt.format(toTime(iso)), tone: \"normal\" }\n    if (diff < 0) return { text: diff === -1 ? \"Overdue by 1 day\" : `Overdue by ${-diff} days`, tone: \"overdue\" }\n    if (diff === 0) return { text: \"Today\", tone: \"today\" }\n    if (diff === 1) return { text: \"Tomorrow\", tone: \"normal\" }\n    return { text: dateFmt.format(toTime(iso)), tone: \"normal\" }\n  }\n\n  const shown = all.filter((t) => filter === \"all\" || t.priority === \"high\")\n  const open = shown.filter((t) => !t.done)\n  const groups = [\n    { key: \"overdue\", title: \"Overdue\", tone: \"text-destructive\", tasks: open.filter((t) => t.due && toTime(t.due) < toTime(today)) },\n    { key: \"today\", title: \"Today\", tone: \"\", tasks: open.filter((t) => t.due === today) },\n    { key: \"upcoming\", title: \"Upcoming\", tone: \"\", tasks: open.filter((t) => t.due && toTime(t.due) > toTime(today)) },\n    { key: \"none\", title: \"No date\", tone: \"\", tasks: open.filter((t) => !t.due) },\n  ].filter((g) => g.tasks.length)\n  const done = shown.filter((t) => t.done)\n  const doneAll = all.filter((t) => t.done).length\n  const pct = all.length ? Math.round((doneAll / all.length) * 100) : 0\n\n  const commitOrder = (list: Tasks1Task[]) => {\n    setTasks(list)\n    onReorder?.(list.map((t) => t.id))\n  }\n\n  function move(task: Tasks1Task, delta: -1 | 1) {\n    // reorder among the tasks that are open and in the same group, so a move is always visible\n    const sameGroup = (a: Tasks1Task) => !a.done && (a.due ?? \"\") === (task.due ?? \"\")\n    const peers = tasks.filter(sameGroup)\n    const at = peers.findIndex((p) => p.id === task.id)\n    const other = peers[at + delta]\n    if (!other) return\n    const next = [...tasks]\n    const i = next.findIndex((t) => t.id === task.id)\n    const j = next.findIndex((t) => t.id === other.id)\n    ;[next[i], next[j]] = [next[j]!, next[i]!]\n    commitOrder(next)\n    setAnnounce(`Moved ${delta < 0 ? \"up\" : \"down\"}`)\n    requestAnimationFrame(() => listRef.current?.querySelector<HTMLElement>(`[data-task-main=\"${task.id}\"]`)?.focus())\n  }\n\n  function toggle(task: Tasks1Task, checked: boolean) {\n    setTasks((l) => l.map((t) => (t.id === task.id ? { ...t, done: checked } : t)))\n    onToggle?.(task.id, checked)\n    setAnnounce(checked ? `Completed: ${task.title}` : `Reopened: ${task.title}`)\n  }\n\n  function remove(task: Tasks1Task) {\n    const index = tasks.findIndex((t) => t.id === task.id)\n    const mains = [...(listRef.current?.querySelectorAll<HTMLElement>(\"[data-task-main]\") ?? [])]\n    const at = mains.findIndex((m) => m.dataset.taskMain === task.id)\n    const next = mains[at + 1] ?? mains[at - 1]\n    setTasks((l) => l.filter((t) => t.id !== task.id))\n    onDelete?.(task.id)\n    setUndo({ task, index })\n    setAnnounce(\"Deleted. Undo is available for a few seconds.\")\n    requestAnimationFrame(() => (next ?? addRef.current)?.focus())\n  }\n\n  function restore() {\n    if (!undo) return\n    const { task, index } = undo\n    setTasks((l) => [...l.slice(0, index), task, ...l.slice(index)])\n    onRestore?.(task)\n    setUndo(null)\n    setAnnounce(\"Task restored\")\n  }\n\n  function add(e: React.FormEvent) {\n    e.preventDefault()\n    const text = draft.trim()\n    if (!text) return\n    const due = dueChoice === \"today\" ? today : dueChoice === \"tomorrow\" ? addDays(today, 1) : dueChoice === \"week\" ? addDays(today, 7) : undefined\n    const task: Tasks1Task = { id: `${id}-new-${++counter.current}`, title: text, due, priority: high ? \"high\" : \"normal\" }\n    setTasks((l) => [task, ...l])\n    onAdd?.(task)\n    setDraft(\"\")\n    setHigh(false)\n    setAnnounce(`Added: ${text}`)\n  }\n\n  function onKeyDown(e: React.KeyboardEvent) {\n    const target = e.target as HTMLElement\n    if (target.tagName === \"INPUT\" || target.tagName === \"TEXTAREA\") return\n    const row = target.closest<HTMLElement>(\"[data-task-row]\")\n    const mains = [...(listRef.current?.querySelectorAll<HTMLElement>(\"[data-task-main]\") ?? [])]\n    const at = mains.findIndex((m) => m.dataset.taskMain === row?.dataset.taskRow)\n    const key = e.key.toLowerCase()\n    const task = tasks.find((t) => t.id === row?.dataset.taskRow)\n    if (e.altKey && (key === \"arrowup\" || key === \"arrowdown\") && task && !task.done) {\n      e.preventDefault()\n      move(task, key === \"arrowup\" ? -1 : 1)\n      return\n    }\n    if (e.metaKey || e.ctrlKey || e.altKey) return\n    if (key === \"j\" || key === \"k\" || key === \"arrowdown\" || key === \"arrowup\") {\n      if (at === -1) return\n      e.preventDefault()\n      mains[Math.max(0, Math.min(mains.length - 1, at + (key === \"j\" || key === \"arrowdown\" ? 1 : -1)))]?.focus()\n    } else if (task && target.hasAttribute(\"data-task-main\")) {\n      if (key === \"x\") {\n        e.preventDefault()\n        toggle(task, !task.done)\n      } else if (key === \"e\") {\n        e.preventDefault()\n        setEditing(task.id)\n        setEditText(task.title)\n      } else if (key === \"delete\" || key === \"backspace\") {\n        e.preventDefault()\n        remove(task)\n      }\n    }\n  }\n\n  function row(task: Tasks1Task, canMove: boolean, first: boolean, last: boolean) {\n    const d = task.due ? dueLabel(task.due, task.done) : null\n    const isEditing = editing === task.id\n    return (\n      <div\n        key={task.id}\n        data-task-row={task.id}\n        className={cn(\"group/task bg-card relative flex items-start gap-3.5 rounded-xl border p-3.5 pe-2 transition-[border-color,background-color] duration-200 focus-within:border-foreground/25 hover:border-foreground/20\", task.done && \"bg-card/50\")}\n      >\n        <Checkbox checked={!!task.done} onCheckedChange={(c) => toggle(task, c === true)} aria-label={`Complete: ${task.title}`} className=\"mt-0.5 size-5\" />\n        <div className=\"min-w-0 flex-1\">\n          {isEditing ? (\n            <input\n              autoFocus\n              aria-label=\"Task title\"\n              value={editText}\n              onChange={(e) => setEditText(e.target.value)}\n              onKeyDown={(e) => {\n                if (e.key === \"Escape\") {\n                  setEditing(null)\n                  requestAnimationFrame(() => listRef.current?.querySelector<HTMLElement>(`[data-task-main=\"${task.id}\"]`)?.focus())\n                } else if (e.key === \"Enter\") {\n                  const text = editText.trim()\n                  if (text) {\n                    setTasks((l) => l.map((t) => (t.id === task.id ? { ...t, title: text } : t)))\n                    onEdit?.(task.id, text)\n                    setAnnounce(\"Task updated\")\n                  }\n                  setEditing(null)\n                  requestAnimationFrame(() => listRef.current?.querySelector<HTMLElement>(`[data-task-main=\"${task.id}\"]`)?.focus())\n                }\n              }}\n              onBlur={() => setEditing(null)}\n              className=\"border-input bg-background focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full rounded-md border px-2 text-sm outline-none focus-visible:ring-[3px]\"\n            />\n          ) : (\n            <button\n              type=\"button\"\n              data-task-main={task.id}\n              onClick={() => {\n                setEditing(task.id)\n                setEditText(task.title)\n              }}\n              aria-label={`${task.title}. Press to edit.`}\n              className=\"focus-visible:ring-ring/50 block w-full rounded-sm text-start outline-none focus-visible:ring-[3px]\"\n            >\n              <span\n                data-done={task.done || undefined}\n                className={cn(\n                  \"relative inline text-[15px] leading-snug text-pretty transition-colors duration-300\",\n                  task.done && \"text-muted-foreground\"\n                )}\n              >\n                <span className={cn(\"bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-[position:0_55%] bg-no-repeat transition-[background-size] duration-300 motion-reduce:transition-none rtl:bg-[position:100%_55%]\", task.done && \"bg-[length:100%_1px]\")}>{task.title}</span>\n              </span>\n            </button>\n          )}\n          {(d || task.priority === \"high\" || task.labels?.length) && !isEditing ? (\n            <div className=\"mt-2 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-xs\">\n              {d ? (\n                <span className={cn(\"flex items-center gap-1.5 tabular-nums\", d.tone === \"overdue\" ? \"text-destructive font-medium\" : d.tone === \"today\" ? \"text-foreground font-medium\" : \"text-muted-foreground\")}>\n                  <CalendarDays aria-hidden=\"true\" className=\"size-3.5\" />\n                  {d.text}\n                </span>\n              ) : null}\n              {task.priority === \"high\" && !task.done ? (\n                <span className=\"text-foreground flex items-center gap-1.5 font-medium\">\n                  <Flag aria-hidden=\"true\" className=\"fill-chart-3 text-chart-3 size-3.5\" />\n                  High priority\n                </span>\n              ) : null}\n              {task.labels?.map((l) => (\n                <span key={l} className=\"bg-muted text-muted-foreground rounded-full px-2 py-0.5\">\n                  {l}\n                </span>\n              ))}\n            </div>\n          ) : null}\n        </div>\n        {!isEditing ? (\n          <div className=\"flex shrink-0 items-center gap-1\">\n            <div className=\"flex gap-0.5 opacity-100 transition-opacity sm:opacity-0 sm:group-focus-within/task:opacity-100 sm:group-hover/task:opacity-100\">\n            {canMove ? (\n              <>\n                <Button variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" aria-label={`Move up: ${task.title}`} disabled={first} onClick={() => move(task, -1)}>\n                  <ChevronUp aria-hidden=\"true\" />\n                </Button>\n                <Button variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" aria-label={`Move down: ${task.title}`} disabled={last} onClick={() => move(task, 1)}>\n                  <ChevronDown aria-hidden=\"true\" />\n                </Button>\n              </>\n            ) : null}\n            <Button variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" aria-label={`Delete: ${task.title}`} onClick={() => remove(task)}>\n              <Trash2 aria-hidden=\"true\" />\n            </Button>\n            </div>\n            {task.assignee ? (\n              <span aria-label={`For ${task.assignee}`} className=\"bg-muted flex size-6 shrink-0 items-center justify-center rounded-full text-[9px] font-semibold\">\n                {task.assignee}\n              </span>\n            ) : null}\n          </div>\n        ) : null}\n      </div>\n    )\n  }\n\n  const empty = ready && !shown.length\n  const content = (\n    <>\n      <BlurFade direction=\"up\" offset={10} duration={0.45}>\n        <div className=\"bg-card mb-8 grid gap-5 rounded-2xl border p-5 sm:grid-cols-[auto_minmax(0,1fr)] sm:items-center sm:gap-6 sm:p-6\">\n          <div className=\"flex items-center gap-4\">\n            <CircularProgress size={72} thickness={8} legend={false} duration={0.8} rings={[{ label: \"Done\", value: doneAll, max: Math.max(1, all.length), tone: \"chart-2\" }]} aria-label={`${doneAll} of ${all.length} tasks done`}>\n              <span className=\"text-base font-semibold tabular-nums\">{pct}%</span>\n            </CircularProgress>\n            <div className=\"sm:hidden\">\n              <p className=\"text-sm font-semibold tabular-nums\">\n                {doneAll} of {all.length} done\n              </p>\n            </div>\n          </div>\n          <form onSubmit={add} className=\"grid gap-2.5\">\n            <p className=\"hidden text-sm font-semibold tabular-nums sm:block\">\n              {doneAll} of {all.length} done\n              <span className=\"text-muted-foreground font-normal\"> · {all.filter((t) => !t.done && t.due && toTime(t.due) < toTime(today)).length} overdue</span>\n            </p>\n            <div className=\"flex flex-wrap items-center gap-2\">\n              <label htmlFor={`${id}-new`} className=\"sr-only\">\n                New task\n              </label>\n              <input\n                id={`${id}-new`}\n                ref={addRef}\n                value={draft}\n                disabled={!ready}\n                onChange={(e) => setDraft(e.target.value)}\n                placeholder=\"Add a task and press Enter\"\n                className=\"border-input bg-background focus-visible:border-ring focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 min-w-0 flex-1 basis-56 rounded-md border px-3 text-sm outline-none focus-visible:ring-[3px] disabled:opacity-50\"\n              />\n              <Select value={dueChoice} onValueChange={(v) => setDueChoice(v as typeof dueChoice)} disabled={!ready}>\n                <SelectTrigger aria-label=\"Due date\" className=\"w-32\">\n                  <SelectValue />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"today\">Today</SelectItem>\n                  <SelectItem value=\"tomorrow\">Tomorrow</SelectItem>\n                  <SelectItem value=\"week\">In a week</SelectItem>\n                  <SelectItem value=\"none\">No date</SelectItem>\n                </SelectContent>\n              </Select>\n              <Button type=\"button\" variant={high ? \"secondary\" : \"outline\"} size=\"icon\" aria-label=\"High priority\" aria-pressed={high} onClick={() => setHigh((h) => !h)} disabled={!ready}>\n                <Flag aria-hidden=\"true\" className={cn(high && \"fill-chart-3 text-chart-3\")} />\n              </Button>\n              <Button type=\"submit\" disabled={!ready || !draft.trim()}>\n                <Plus aria-hidden=\"true\" />\n                Add\n              </Button>\n            </div>\n          </form>\n        </div>\n      </BlurFade>\n\n      <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n        <SegmentedControl value={filter} onValueChange={(v) => setFilter(v as typeof filter)} aria-label=\"Show\" size=\"sm\" disabled={!ready}>\n          <SegmentedControlItem value=\"all\">All</SegmentedControlItem>\n          <SegmentedControlItem value=\"high\">High priority</SegmentedControlItem>\n        </SegmentedControl>\n        <p className=\"text-muted-foreground ms-auto hidden items-center gap-1.5 text-xs lg:flex\">\n          <Kbd>N</Kbd> new <span aria-hidden=\"true\">·</span> <Kbd>X</Kbd> complete <span aria-hidden=\"true\">·</span> <Kbd>E</Kbd> edit <span aria-hidden=\"true\">·</span> <Kbd>Alt</Kbd>\n          <Kbd>↑</Kbd> move\n        </p>\n      </div>\n\n      {empty ? (\n        <div className=\"bg-card rounded-2xl border px-6 py-16 text-center\">\n          <span aria-hidden=\"true\" className=\"bg-muted mx-auto flex size-12 items-center justify-center rounded-full\">\n            {all.length ? <ListTodo className=\"size-6\" /> : <CircleCheckBig className=\"size-6\" />}\n          </span>\n          <h2 className=\"mt-5 text-lg font-semibold tracking-tight\">{emptyState?.title ?? (all.length ? \"No high priority tasks\" : \"Nothing to do\")}</h2>\n          <p className=\"text-muted-foreground mx-auto mt-1.5 max-w-xs text-sm text-pretty\">{emptyState?.description ?? (all.length ? \"Everything on your list is at a normal pace.\" : \"Add a task above, or enjoy the quiet.\")}</p>\n          {emptyState?.action}\n        </div>\n      ) : (\n        <div ref={listRef} onKeyDown={onKeyDown} className=\"grid grid-cols-1 gap-9\">\n          {groups.map((g) => (\n            <section key={g.key} aria-labelledby={`${id}-${g.key}`}>\n              <h2 id={`${id}-${g.key}`} className={cn(\"mb-3 flex items-center gap-2 text-xs font-semibold\", g.tone || \"text-muted-foreground\")}>\n                {g.title}\n                <span className=\"text-muted-foreground font-normal tabular-nums\">{g.tasks.length}</span>\n              </h2>\n              <ul className=\"grid list-none grid-cols-1 gap-2\">\n                {g.tasks.map((t, i) => (\n                  <li key={t.id}>{row(t, true, i === 0, i === g.tasks.length - 1)}</li>\n                ))}\n              </ul>\n            </section>\n          ))}\n          {done.length ? (\n            <section aria-labelledby={`${id}-done`}>\n              <h2 id={`${id}-done`} className=\"mb-3\">\n                <button type=\"button\" aria-expanded={showDone} aria-controls={`${id}-done-list`} onClick={() => setShowDone((s) => !s)} className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 flex items-center gap-2 rounded-sm text-xs font-semibold outline-none focus-visible:ring-[3px]\">\n                  <ChevronDown aria-hidden=\"true\" className={cn(\"size-4 transition-transform duration-150 motion-reduce:transition-none\", showDone ? \"\" : \"-rotate-90 rtl:rotate-90\")} />\n                  Completed\n                  <span className=\"font-normal tabular-nums\">{done.length}</span>\n                </button>\n              </h2>\n              {showDone ? (\n                <ul id={`${id}-done-list`} className=\"animate-in fade-in-0 slide-in-from-top-1 grid list-none grid-cols-1 gap-2 duration-200 motion-reduce:animate-none\">\n                  {done.map((t) => (\n                    <li key={t.id}>{row(t, false, true, true)}</li>\n                  ))}\n                </ul>\n              ) : null}\n            </section>\n          ) : null}\n        </div>\n      )}\n    </>\n  )\n\n  return (\n    <section data-slot=\"tasks-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-3xl px-4 py-8 pb-24 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-6\">\n        <h1 id={`${id}-title`} className=\"text-3xl font-semibold tracking-[-0.035em]\">\n          {title}\n        </h1>\n      </header>\n      <StateView state={state === \"loading\" || state === \"error\" ? state : \"ready\"} onRetry={onRetry} label=\"Loading tasks\" loading={<TasksSkeleton />} error={{ title: errorState?.title ?? \"Your tasks could not be loaded\", description: errorState?.description ?? \"Nothing was lost. Check your connection and try again.\", ...errorState }}>\n        {content}\n      </StateView>\n      {undo ? (\n        <div role=\"status\" className=\"bg-popover text-popover-foreground fixed inset-x-4 bottom-4 z-30 mx-auto flex max-w-md items-center gap-3 rounded-xl border p-3 ps-4 shadow-lg\">\n          <p className=\"min-w-0 flex-1 truncate text-sm\">Deleted “{undo.task.title}”</p>\n          <Button variant=\"outline\" size=\"sm\" onClick={restore}>\n            <Undo2 aria-hidden=\"true\" className=\"rtl:-scale-x-100\" />\n            Undo\n          </Button>\n        </div>\n      ) : null}\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </p>\n    </section>\n  )\n}\n\nexport { Tasks1, type Tasks1Props, type Tasks1Task }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/tasks-1/tasks-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "tasks",
      "todo",
      "to-do",
      "checklist",
      "productivity",
      "due dates",
      "priority",
      "keyboard",
      "reorder",
      "list"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "A personal task list. <Tasks1 tasks={[{ id, title, done, due: '2026-10-18', priority: 'normal' | 'high', assignee: 'JL', labels }]} today=\"2026-10-18\" locale onAdd onToggle onEdit onDelete onRestore onReorder />. Pass today yourself (an ISO date) so the server and browser agree about what is overdue; the block never reads the clock. Groups come from due dates.",
      "whenToUse": [
        "A personal or team to-do list inside an app",
        "A today view with overdue items called out"
      ],
      "whenNotToUse": [
        "A project board (use kanban-1)",
        "A one-time setup checklist (use onboarding-checklist)",
        "Gantt or schedule views"
      ],
      "composesWith": [
        "app-shell-1",
        "kanban-1",
        "calendar-1",
        "inbox-1"
      ],
      "a11y": [
        {
          "keys": "J / K or arrow keys",
          "action": "Move between tasks; X completes, E edits, Delete removes (with Undo), Alt+Up and Alt+Down reorder, N jumps to the add box"
        },
        {
          "keys": "Reorder buttons",
          "action": "Move up and Move down buttons exist for every task, so ordering never needs a pointer or a drag"
        },
        {
          "keys": "Due and priority",
          "action": "Overdue and High priority are written as words next to their icon, not color alone; completing a task is announced"
        }
      ],
      "customization": [
        "today, locale, title",
        "defaultShowCompleted opens the Completed section",
        "Reordering applies within a group (same due date), so every move is visible; persist the new order from onReorder"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "tasks-1-loading",
        "trigger": "state=\"loading\"",
        "note": "A skeleton of the summary and the task rows under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "Nothing to do",
        "example": "tasks-1-empty",
        "trigger": "state=\"empty\" (or tasks={[]})",
        "note": "A calm message pointing at the add box; a filter with no matches says so instead. emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "tasks-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was lost, with Try again (onRetry)."
      }
    ],
    "examples": [
      "tasks-1-demo",
      "tasks-1-completed",
      "tasks-1-empty",
      "tasks-1-loading",
      "tasks-1-error"
    ],
    "url": "https://ui.ballmac.com/components/tasks-1"
  },
  "categories": [
    "blocks",
    "tasks"
  ],
  "type": "registry:block"
}