{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"approval-queue","type":"registry:block","title":"Approval Queue","description":"Review queue for pending expense, access, deploy and time-off requests with row and bulk selection, approve and reject that swipe rows out optimistically with an undo toast, a dialog that asks for a rejection reason, Pending/Approved/Rejected tabs whose counts roll, and j/k/x/a/r keyboard shortcuts.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/approval-queue.tsx","type":"registry:component","target":"@components/motion/approval-queue.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/blocks/approval-queue\n\nimport {\n  Check,\n  CreditCard,\n  Inbox,\n  KeyRound,\n  Plane,\n  RotateCcw,\n  Rocket,\n  ShoppingCart,\n  X,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion, type Variants } from \"motion/react\";\nimport {\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  AnimatedToastStack,\n  useAnimatedToastStack,\n} from \"@/components/motion/animated-toast-stack\";\nimport { Button } from \"@/components/motion/button\";\nimport { Checkbox } from \"@/components/motion/checkbox\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/motion/dialog\";\nimport { EmptyState } from \"@/components/motion/empty-state\";\nimport { Kbd } from \"@/components/motion/kbd\";\nimport { NumberTicker } from \"@/components/motion/number-ticker\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/motion/tabs\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ApprovalStatus = \"pending\" | \"approved\" | \"rejected\";\nexport type ApprovalPriority = \"low\" | \"normal\" | \"high\" | \"urgent\";\nexport type ApprovalKind = \"expense\" | \"access\" | \"deploy\" | \"purchase\" | \"time-off\";\n\nexport interface ApprovalRequest {\n  id: string;\n  kind: ApprovalKind;\n  title: string;\n  requester: { name: string; avatar?: string };\n  /** Amount in major units (e.g. dollars). Omit for non-monetary requests. */\n  amount?: number;\n  /** ISO 4217 code. Default: the component's `currency`. */\n  currency?: string;\n  /** One-line context, e.g. \"prod · v2.14.0\" or \"Admin on billing\". */\n  details?: string;\n  /** Date, epoch ms or ISO string — drives the age column. */\n  createdAt: Date | number | string;\n  priority: ApprovalPriority;\n  status: ApprovalStatus;\n  /** Set when rejected. */\n  reason?: string;\n}\n\nexport interface ApprovalQueueProps {\n  /** Controlled list. Decisions call `onRequestsChange` with updated statuses. */\n  requests?: ApprovalRequest[];\n  /** Uncontrolled initial list. */\n  defaultRequests?: ApprovalRequest[];\n  onRequestsChange?: (requests: ApprovalRequest[]) => void;\n  onApprove?: (ids: string[]) => void;\n  onReject?: (ids: string[], reason: string) => void;\n  /** Fired when a decision is undone from the toast or reopened. */\n  onUndo?: (ids: string[]) => void;\n  /** Default currency for amounts. Default \"USD\". */\n  currency?: string;\n  /** Locale for numbers. Default \"en-US\". */\n  locale?: string;\n  /** Height of the scrolling list. Default \"16rem\". */\n  listHeight?: string;\n  /** Show the j/k/a/r keyboard legend. Default true. */\n  showShortcuts?: boolean;\n  className?: string;\n}\n\nconst KIND_ICON: Record<ApprovalKind, typeof CreditCard> = {\n  expense: CreditCard,\n  access: KeyRound,\n  deploy: Rocket,\n  purchase: ShoppingCart,\n  \"time-off\": Plane,\n};\n\nconst PRIORITY_CLASS: Record<ApprovalPriority, string> = {\n  low: \"bg-foreground/5 text-muted-foreground\",\n  normal:\n    \"bg-[color-mix(in_oklab,var(--chart-1)_12%,transparent)] text-[color-mix(in_oklab,var(--chart-1)_70%,var(--foreground))]\",\n  high: \"bg-[color-mix(in_oklab,var(--warning)_18%,transparent)] text-[color-mix(in_oklab,var(--warning)_50%,var(--foreground))]\",\n  urgent:\n    \"bg-[color-mix(in_oklab,var(--destructive)_14%,transparent)] text-[color-mix(in_oklab,var(--destructive)_75%,var(--foreground))]\",\n};\n\nconst REASONS = [\"Over budget\", \"Needs more detail\", \"Duplicate request\", \"Not now\"];\n\nfunction toMs(t: ApprovalRequest[\"createdAt\"]) {\n  return t instanceof Date ? t.getTime() : typeof t === \"number\" ? t : Date.parse(t);\n}\n\nfunction age(ms: number) {\n  const m = Math.max(0, Math.round(ms / 60_000));\n  if (m < 60) return `${m}m`;\n  const h = Math.round(m / 60);\n  if (h < 24) return `${h}h`;\n  return `${Math.round(h / 24)}d`;\n}\n\nfunction useControllable<T>(value: T | undefined, defaultValue: T, onChange?: (next: T) => void) {\n  const [internal, setInternal] = useState(defaultValue);\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n  const set = useCallback(\n    (next: T) => {\n      if (!controlled) setInternal(next);\n      onChange?.(next);\n    },\n    [controlled, onChange],\n  );\n  return [current, set] as const;\n}\n\n/**\n * Review queue for pending requests (expenses, access grants, deploys…).\n * Approve or reject one row or a selection: rows swipe out optimistically —\n * right for approve, left for reject — and an undo toast puts them back.\n * Rejecting asks for a reason in a Dialog. Pending/Approved/Rejected tabs\n * roll their counts. Keyboard: j/k move, x selects, a approves, r rejects.\n */\nexport function ApprovalQueue({\n  requests,\n  defaultRequests = [],\n  onRequestsChange,\n  onApprove,\n  onReject,\n  onUndo,\n  currency = \"USD\",\n  locale = \"en-US\",\n  listHeight = \"16rem\",\n  showShortcuts = true,\n  className,\n}: ApprovalQueueProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const [list, setList] = useControllable(requests, defaultRequests, onRequestsChange);\n  const [tab, setTab] = useState<ApprovalStatus>(\"pending\");\n  const [selected, setSelected] = useState<Set<string>>(() => new Set());\n  const [activeId, setActiveId] = useState<string | null>(null);\n  // Direction the next exiting rows swipe: 1 = approve (right), -1 = reject.\n  const [exitDir, setExitDir] = useState<1 | -1>(1);\n  const [rejecting, setRejecting] = useState<string[] | null>(null);\n  const [reason, setReason] = useState(\"\");\n  const { toasts, showToast, dismissToast } = useAnimatedToastStack({ defaultDuration: 5000, limit: 3 });\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const rowRefs = useRef(new Map<string, HTMLButtonElement>());\n  // Latest list for toast callbacks fired after later decisions.\n  const listRef = useRef(list);\n  listRef.current = list;\n\n  const [now, setNow] = useState<number | null>(null);\n  useEffect(() => {\n    setNow(Date.now());\n    const t = window.setInterval(() => setNow(Date.now()), 60_000);\n    return () => window.clearInterval(t);\n  }, []);\n\n  const counts = useMemo(() => {\n    const c = { pending: 0, approved: 0, rejected: 0 };\n    for (const r of list) c[r.status]++;\n    return c;\n  }, [list]);\n\n  const visible = useMemo(\n    () =>\n      list\n        .filter((r) => r.status === tab)\n        .sort((a, b) => toMs(a.createdAt) - toMs(b.createdAt)),\n    [list, tab],\n  );\n  const pendingTab = tab === \"pending\";\n  const selectedVisible = visible.filter((r) => selected.has(r.id)).map((r) => r.id);\n\n  const money = useMemo(() => {\n    const cache = new Map<string, Intl.NumberFormat>();\n    return (amount: number, code: string) => {\n      let f = cache.get(code);\n      if (!f) {\n        f = new Intl.NumberFormat(locale, { style: \"currency\", currency: code, maximumFractionDigits: 0 });\n        cache.set(code, f);\n      }\n      return f.format(amount);\n    };\n  }, [locale]);\n\n  const focusRow = (id: string | null) => {\n    setActiveId(id);\n    if (id) requestAnimationFrame(() => rowRefs.current.get(id)?.focus({ preventScroll: false }));\n  };\n\n  /** After rows leave, keep the cursor on the next remaining row. */\n  const nextActive = (leaving: string[]) => {\n    if (!activeId || !leaving.includes(activeId)) return activeId;\n    const idx = visible.findIndex((r) => r.id === activeId);\n    const rest = visible.filter((r) => !leaving.includes(r.id));\n    return rest[Math.min(idx, rest.length - 1)]?.id ?? null;\n  };\n\n  const decide = (ids: string[], status: \"approved\" | \"rejected\", why?: string) => {\n    if (!ids.length) return;\n    const before = new Map(list.filter((r) => ids.includes(r.id)).map((r) => [r.id, r]));\n    setExitDir(status === \"approved\" ? 1 : -1);\n    const next = nextActive(ids);\n    // Moving focus before the rows leave keeps it from landing on <body>.\n    const leavingHasFocus = ids.some((id) => rowRefs.current.get(id)?.contains(document.activeElement));\n    setList(list.map((r) => (ids.includes(r.id) ? { ...r, status, reason: why } : r)));\n    setSelected((prev) => {\n      const s = new Set(prev);\n      for (const id of ids) s.delete(id);\n      return s;\n    });\n    if (leavingHasFocus) {\n      if (next) focusRow(next);\n      else rootRef.current?.focus();\n    } else setActiveId(next);\n    if (status === \"approved\") onApprove?.(ids);\n    else onReject?.(ids, why ?? \"\");\n\n    const first = before.values().next().value;\n    const label = ids.length === 1 && first ? `“${first.title}”` : `${ids.length} requests`;\n    const toastId = showToast({\n      title: `${status === \"approved\" ? \"Approved\" : \"Rejected\"} ${label}`,\n      description: why ? `Reason: ${why}` : undefined,\n      status: status === \"approved\" ? \"success\" : \"error\",\n      action: {\n        label: \"Undo\",\n        onClick: () => {\n          setExitDir(status === \"approved\" ? -1 : 1);\n          setList(listRef.current.map((r) => before.get(r.id) ?? r));\n          onUndo?.(ids);\n          dismissToast(toastId);\n        },\n      },\n    });\n  };\n\n  const reopen = (id: string) => {\n    setExitDir(tab === \"approved\" ? -1 : 1);\n    setList(list.map((r) => (r.id === id ? { ...r, status: \"pending\", reason: undefined } : r)));\n    onUndo?.([id]);\n  };\n\n  const askReject = (ids: string[]) => {\n    if (!ids.length) return;\n    setReason(\"\");\n    setRejecting(ids);\n  };\n  const confirmReject = () => {\n    if (!rejecting) return;\n    decide(rejecting, \"rejected\", reason.trim() || undefined);\n    setRejecting(null);\n  };\n\n  const toggleSelect = (id: string) =>\n    setSelected((prev) => {\n      const s = new Set(prev);\n      if (s.has(id)) s.delete(id);\n      else s.add(id);\n      return s;\n    });\n  const allSelected = visible.length > 0 && selectedVisible.length === visible.length;\n  const toggleAll = () =>\n    setSelected((prev) => {\n      const s = new Set(prev);\n      if (allSelected) for (const r of visible) s.delete(r.id);\n      else for (const r of visible) s.add(r.id);\n      return s;\n    });\n\n  // j/k/x/a/r while focus is anywhere inside the queue (not in a text field).\n  // Attached natively so the wrapper stays a plain, non-interactive element.\n  const keyState = useRef({ visible, activeId, selectedVisible, pendingTab, decide, askReject, toggleSelect, focusRow });\n  keyState.current = { visible, activeId, selectedVisible, pendingTab, decide, askReject, toggleSelect, focusRow };\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.metaKey || e.ctrlKey || e.altKey) return;\n      const t = e.target as HTMLElement;\n      if (t.closest(\"input, textarea, select, [contenteditable='true']\")) return;\n      const s = keyState.current;\n      const idx = s.visible.findIndex((r) => r.id === s.activeId);\n      const key = e.key.toLowerCase();\n      if (key === \"j\" || key === \"k\") {\n        e.preventDefault();\n        if (!s.visible.length) return;\n        const next =\n          idx === -1 ? 0 : Math.min(s.visible.length - 1, Math.max(0, idx + (key === \"j\" ? 1 : -1)));\n        s.focusRow(s.visible[next].id);\n        return;\n      }\n      if (!s.pendingTab) return;\n      const targets = s.selectedVisible.length ? s.selectedVisible : s.activeId && idx !== -1 ? [s.activeId] : [];\n      if (key === \"x\" && s.activeId && idx !== -1) {\n        e.preventDefault();\n        s.toggleSelect(s.activeId);\n      } else if (key === \"a\" && targets.length) {\n        e.preventDefault();\n        s.decide(targets, \"approved\");\n      } else if (key === \"r\" && targets.length) {\n        e.preventDefault();\n        s.askReject(targets);\n      }\n    };\n    root.addEventListener(\"keydown\", onKey);\n    return () => root.removeEventListener(\"keydown\", onKey);\n  }, []);\n\n  const rowVariants: Variants = {\n    initial: { opacity: 0, y: reduce ? 0 : 6 },\n    animate: { opacity: 1, x: 0, y: 0 },\n    exit: (dir: number) => ({\n      opacity: 0,\n      x: reduce ? 0 : dir * 96,\n      transition: { duration: 0.2, ease: EASE_OUT },\n    }),\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      tabIndex={-1}\n      className={cn(\n        \"relative flex w-full flex-col gap-3 overflow-hidden rounded-2xl border border-border bg-card p-4 text-card-foreground outline-none\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center justify-between gap-3\">\n        <Tabs value={tab} onValueChange={(v) => setTab(v as ApprovalStatus)} variant=\"underline\">\n          <TabsList>\n            {([\"pending\", \"approved\", \"rejected\"] as const).map((s) => (\n              <TabsTrigger key={s} value={s} className=\"min-h-9 gap-1.5 px-2 text-xs capitalize sm:text-sm\">\n                {s}\n                <span className=\"inline-flex min-w-5 items-center justify-center rounded-full bg-foreground/5 px-1.5 text-[11px] tabular-nums text-muted-foreground\">\n                  <NumberTicker value={counts[s]} startOnView={false} duration={0.45} stagger={0.02} />\n                </span>\n              </TabsTrigger>\n            ))}\n          </TabsList>\n        </Tabs>\n        {pendingTab && visible.length > 0 ? (\n          <Checkbox\n            checked={allSelected}\n            indeterminate={!allSelected && selectedVisible.length > 0}\n            onCheckedChange={toggleAll}\n            aria-label=\"Select all pending requests\"\n            label=\"All\"\n            className=\"gap-2 text-xs\"\n          />\n        ) : null}\n      </div>\n\n      {/* Bulk bar slides down when rows are selected. */}\n      <AnimatePresence initial={false}>\n        {pendingTab && selectedVisible.length > 0 ? (\n          <motion.div\n            key=\"bulk\"\n            initial={{ opacity: 0, y: reduce ? 0 : -6 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, transition: { duration: 0.12 } }}\n            transition={{ duration: 0.2, ease: EASE_OUT }}\n            className=\"flex items-center justify-between gap-2 rounded-xl bg-foreground/[0.04] py-1.5 pr-1.5 pl-3\"\n          >\n            <span className=\"text-xs font-medium tabular-nums\" aria-live=\"polite\">\n              {selectedVisible.length} selected\n            </span>\n            <span className=\"flex gap-1.5\">\n              <Button size=\"sm\" variant=\"outline\" onClick={() => askReject(selectedVisible)}>\n                <X className=\"size-3.5\" aria-hidden />\n                Reject\n              </Button>\n              <Button size=\"sm\" onClick={() => decide(selectedVisible, \"approved\")}>\n                <Check className=\"size-3.5\" aria-hidden />\n                Approve\n              </Button>\n            </span>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n\n      <div\n        className=\"-mx-1 overflow-x-hidden overflow-y-auto px-1 [scrollbar-color:var(--border-strong)_transparent] [scrollbar-width:thin]\"\n        style={{ height: listHeight }}\n      >\n        {visible.length === 0 ? (\n          <EmptyState\n            size=\"sm\"\n            icon={<Inbox className=\"size-5\" />}\n            title={pendingTab ? \"Queue is clear\" : `Nothing ${tab} yet`}\n            description={pendingTab ? \"New requests will show up here.\" : undefined}\n          />\n        ) : (\n          <ul className=\"flex flex-col gap-1\" aria-label={`${tab} requests`}>\n            <AnimatePresence initial={false} mode=\"popLayout\" custom={exitDir}>\n              {visible.map((r) => (\n                <ApprovalRow\n                  key={r.id}\n                  request={r}\n                  variants={rowVariants}\n                  exitDir={exitDir}\n                  active={r.id === activeId}\n                  selected={selected.has(r.id)}\n                  pendingTab={pendingTab}\n                  reduce={reduce}\n                  layoutGroup={baseId}\n                  ageLabel={now === null ? \"\" : age(now - toMs(r.createdAt))}\n                  amountLabel={r.amount === undefined ? null : money(r.amount, r.currency ?? currency)}\n                  registerRow={(el) => {\n                    if (el) rowRefs.current.set(r.id, el);\n                    else rowRefs.current.delete(r.id);\n                  }}\n                  onFocusRow={() => setActiveId(r.id)}\n                  onToggle={() => toggleSelect(r.id)}\n                  onApprove={() => decide([r.id], \"approved\")}\n                  onReject={() => askReject([r.id])}\n                  onReopen={() => reopen(r.id)}\n                />\n              ))}\n            </AnimatePresence>\n          </ul>\n        )}\n      </div>\n\n      {showShortcuts ? (\n        <p className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-muted-foreground\">\n          <span className=\"inline-flex items-center gap-1\">\n            <Kbd size=\"sm\">j</Kbd>\n            <Kbd size=\"sm\">k</Kbd> move\n          </span>\n          <span className=\"inline-flex items-center gap-1\">\n            <Kbd size=\"sm\">x</Kbd> select\n          </span>\n          <span className=\"inline-flex items-center gap-1\">\n            <Kbd size=\"sm\">a</Kbd> approve\n          </span>\n          <span className=\"inline-flex items-center gap-1\">\n            <Kbd size=\"sm\">r</Kbd> reject\n          </span>\n        </p>\n      ) : null}\n\n      <AnimatedToastStack\n        toasts={toasts}\n        onDismiss={dismissToast}\n        placement=\"absolute\"\n        position=\"bottom-center\"\n        maxVisible={2}\n        className=\"w-[calc(100%-2rem)]\"\n      />\n\n      <Dialog open={rejecting !== null} onOpenChange={(o) => !o && setRejecting(null)}>\n        <DialogContent className=\"max-w-md\">\n          <DialogHeader>\n            <DialogTitle>\n              Reject {rejecting && rejecting.length > 1 ? `${rejecting.length} requests` : \"request\"}\n            </DialogTitle>\n            <DialogDescription>\n              The requester sees this reason. You can undo right after.\n            </DialogDescription>\n          </DialogHeader>\n          <div className=\"mt-4 flex flex-col gap-2\">\n            <div className=\"flex flex-wrap gap-1.5\">\n              {REASONS.map((r) => (\n                <button\n                  key={r}\n                  type=\"button\"\n                  aria-pressed={reason === r}\n                  onClick={() => setReason(r)}\n                  className={cn(\n                    \"h-7 rounded-full border border-border px-2.5 text-xs outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n                    reason === r\n                      ? \"border-foreground/30 bg-foreground/10 text-foreground\"\n                      : \"border-border text-muted-foreground hover:text-foreground\",\n                  )}\n                >\n                  {r}\n                </button>\n              ))}\n            </div>\n            <label className=\"flex flex-col gap-1.5 text-sm font-medium\">\n              Reason\n              <textarea\n                data-dialog-autofocus\n                rows={3}\n                value={reason}\n                onChange={(e) => setReason(e.target.value)}\n                onKeyDown={(e) => {\n                  if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey)) confirmReject();\n                }}\n                placeholder=\"Add context for the requester…\"\n                className=\"resize-none rounded-xl border border-border bg-background px-3 py-2 text-sm font-normal outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/40\"\n              />\n            </label>\n          </div>\n          <DialogFooter className=\"mt-5\">\n            <Button variant=\"outline\" size=\"sm\" onClick={() => setRejecting(null)}>\n              Cancel\n            </Button>\n            <Button size=\"sm\" onClick={confirmReject} disabled={!reason.trim()}>\n              Reject\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n}\n\nfunction ApprovalRow({\n  ref,\n  request: r,\n  variants,\n  exitDir,\n  active,\n  selected,\n  pendingTab,\n  reduce,\n  layoutGroup,\n  ageLabel,\n  amountLabel,\n  registerRow,\n  onFocusRow,\n  onToggle,\n  onApprove,\n  onReject,\n  onReopen,\n}: {\n  /** Forwarded so AnimatePresence's popLayout can measure the exiting row. */\n  ref?: Ref<HTMLLIElement>;\n  request: ApprovalRequest;\n  variants: Variants;\n  exitDir: number;\n  active: boolean;\n  selected: boolean;\n  pendingTab: boolean;\n  reduce: boolean;\n  layoutGroup: string;\n  ageLabel: string;\n  amountLabel: string | null;\n  registerRow: (el: HTMLButtonElement | null) => void;\n  onFocusRow: () => void;\n  onToggle: () => void;\n  onApprove: () => void;\n  onReject: () => void;\n  onReopen: () => void;\n}) {\n  const Icon = KIND_ICON[r.kind];\n  const description: ReactNode = (\n    <>\n      {r.requester.name}\n      {r.details ? <> · {r.details}</> : null}\n    </>\n  );\n\n  return (\n    <motion.li\n      ref={ref}\n      layout={reduce ? false : \"position\"}\n      custom={exitDir}\n      variants={variants}\n      initial=\"initial\"\n      animate=\"animate\"\n      exit=\"exit\"\n      transition={{ default: { duration: 0.22, ease: EASE_OUT }, layout: SPRING_LAYOUT }}\n      className=\"group relative flex items-center gap-3 rounded-xl px-2 py-2\"\n    >\n      {active ? (\n        <motion.span\n          aria-hidden\n          layoutId={`${layoutGroup}-active`}\n          transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n          className=\"absolute inset-0 -z-0 rounded-xl bg-foreground/[0.05] ring-1 ring-border-strong\"\n        />\n      ) : null}\n      {pendingTab ? (\n        <Checkbox\n          checked={selected}\n          onCheckedChange={onToggle}\n          aria-label={`Select ${r.title}`}\n          className=\"relative\"\n        />\n      ) : null}\n      <span\n        aria-hidden\n        className=\"relative grid size-8 shrink-0 place-items-center rounded-lg bg-foreground/5 text-muted-foreground\"\n      >\n        <Icon className=\"size-4\" />\n      </span>\n      <button\n        ref={registerRow}\n        type=\"button\"\n        onFocus={onFocusRow}\n        onClick={pendingTab ? onToggle : onFocusRow}\n        aria-pressed={pendingTab ? selected : undefined}\n        className=\"relative min-w-0 flex-1 rounded-md text-left outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <span className=\"flex items-center gap-2\">\n          <span className=\"truncate text-sm font-medium\">{r.title}</span>\n          <span\n            className={cn(\n              \"inline-flex h-4.5 shrink-0 items-center rounded-full px-1.5 text-[10px] font-semibold capitalize\",\n              PRIORITY_CLASS[r.priority],\n            )}\n          >\n            {r.priority}\n          </span>\n        </span>\n        <span className=\"block truncate text-xs text-muted-foreground\">\n          {description}\n          {r.status === \"rejected\" && r.reason ? <> · “{r.reason}”</> : null}\n        </span>\n      </button>\n      <span className=\"relative flex shrink-0 flex-col items-end\">\n        {amountLabel ? <span className=\"text-sm font-semibold tabular-nums\">{amountLabel}</span> : null}\n        <span className=\"text-[11px] tabular-nums text-muted-foreground\">\n          <span className=\"sr-only\">Waiting </span>\n          {ageLabel}\n        </span>\n      </span>\n      <span className=\"relative flex shrink-0 gap-1\">\n        {pendingTab ? (\n          <>\n            <IconAction label={`Reject ${r.title}`} onClick={onReject} tone=\"reject\">\n              <X className=\"size-4\" />\n            </IconAction>\n            <IconAction label={`Approve ${r.title}`} onClick={onApprove} tone=\"approve\">\n              <Check className=\"size-4\" />\n            </IconAction>\n          </>\n        ) : (\n          <IconAction label={`Move ${r.title} back to pending`} onClick={onReopen}>\n            <RotateCcw className=\"size-3.5\" />\n          </IconAction>\n        )}\n      </span>\n    </motion.li>\n  );\n}\n\nfunction IconAction({\n  label,\n  onClick,\n  tone,\n  children,\n}: {\n  label: string;\n  onClick: () => void;\n  tone?: \"approve\" | \"reject\";\n  children: ReactNode;\n}) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      title={label}\n      onClick={onClick}\n      className={cn(\n        \"grid size-8 place-items-center rounded-lg border border-border text-muted-foreground outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n        tone === \"approve\" &&\n          \"hover:border-[color-mix(in_oklab,var(--success)_40%,transparent)] hover:bg-[color-mix(in_oklab,var(--success)_14%,transparent)] hover:text-[color-mix(in_oklab,var(--success)_55%,var(--foreground))]\",\n        tone === \"reject\" &&\n          \"hover:border-[color-mix(in_oklab,var(--destructive)_40%,transparent)] hover:bg-[color-mix(in_oklab,var(--destructive)_12%,transparent)] hover:text-[color-mix(in_oklab,var(--destructive)_75%,var(--foreground))]\",\n        !tone && \"hover:bg-foreground/5 hover:text-foreground\",\n      )}\n    >\n      <span aria-hidden className=\"inline-flex\">\n        {children}\n      </span>\n    </button>\n  );\n}\n"},{"path":"components/motion/animated-toast-stack.tsx","type":"registry:component","target":"@components/motion/animated-toast-stack.tsx","content":"\"use client\";\n\nimport {\n  AlertCircle,\n  Bell,\n  Check,\n  Info,\n  LoaderCircle,\n  X,\n  type LucideIcon,\n} from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Transition,\n} from \"motion/react\";\nimport {\n  isValidElement,\n  memo,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ToastStatus = \"neutral\" | \"info\" | \"loading\" | \"success\" | \"error\";\nexport type ToastPosition =\n  | \"top-left\"\n  | \"top-center\"\n  | \"top-right\"\n  | \"bottom-left\"\n  | \"bottom-center\"\n  | \"bottom-right\";\n\nexport type AnimatedToastAction = {\n  label: ReactNode;\n  onClick: (toast: AnimatedToast) => void;\n};\n\nexport type AnimatedToast = {\n  id: string;\n  title: ReactNode;\n  description?: ReactNode;\n  status?: ToastStatus;\n  icon?: ReactNode;\n  action?: AnimatedToastAction;\n  duration?: number;\n  dismissible?: boolean;\n  createdAt?: number;\n};\n\nexport type ToastInput = Omit<AnimatedToast, \"id\" | \"createdAt\"> & {\n  id?: string;\n};\n\n/** Copy for one phase of a promise toast: a plain title, or a partial toast. */\nexport type ToastPromiseContent =\n  | ReactNode\n  | (Omit<ToastInput, \"id\" | \"status\"> & { title: ReactNode });\n\nexport type ToastPromiseMessages<T> = {\n  loading: ToastPromiseContent;\n  success: ToastPromiseContent | ((data: T) => ToastPromiseContent);\n  error: ToastPromiseContent | ((error: unknown) => ToastPromiseContent);\n  /** Auto-dismiss delay once the promise settles. Defaults to the hook's defaultDuration. */\n  duration?: number;\n  /** Reuse an existing toast id instead of opening a new one. */\n  id?: string;\n};\n\nexport type ToastPromiseResult<T> = {\n  id: string;\n  /** The original promise, so callers can still `await` the work. */\n  result: Promise<T>;\n};\n\nexport type ToastClassNames = {\n  root?: string;\n  item?: string;\n  surface?: string;\n  iconWrap?: string;\n  content?: string;\n  title?: string;\n  description?: string;\n  action?: string;\n  close?: string;\n  progress?: string;\n};\n\nexport interface AnimatedToastStackProps {\n  toasts: AnimatedToast[];\n  onDismiss?: (id: string) => void;\n  position?: ToastPosition;\n  placement?: \"static\" | \"fixed\" | \"absolute\";\n  fixed?: boolean;\n  portal?: boolean;\n  portalRoot?: Element | null;\n  maxVisible?: number;\n  className?: string;\n  classNames?: ToastClassNames;\n  icons?: Partial<Record<ToastStatus, ReactNode>>;\n  renderToast?: (toast: AnimatedToast) => ReactNode;\n}\n\nexport interface UseAnimatedToastStackOptions {\n  initialToasts?: ToastInput[];\n  defaultDuration?: number;\n  limit?: number;\n}\n\nconst STACK_SPRING: Transition = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 34,\n  mass: 0.75,\n};\n\nconst CONTENT_TRANSITION = {\n  duration: 0.28,\n  ease: EASE_OUT,\n} as const;\n\nconst STATUS_ICON: Record<ToastStatus, LucideIcon> = {\n  neutral: Bell,\n  info: Info,\n  loading: LoaderCircle,\n  success: Check,\n  error: AlertCircle,\n};\n\nconst STATUS_CLASS: Record<ToastStatus, string> = {\n  neutral: \"text-muted-foreground bg-primary/[0.05]\",\n  info: \"text-primary bg-primary/10\",\n  loading: \"text-primary bg-primary/10\",\n  success: \"text-emerald-600 bg-emerald-500/10 dark:text-emerald-400\",\n  error: \"text-destructive bg-destructive/10\",\n};\n\nconst POSITION_CLASS: Record<ToastPosition, string> = {\n  \"top-left\": \"left-4 top-4\",\n  \"top-center\": \"left-1/2 top-4 -translate-x-1/2\",\n  \"top-right\": \"right-4 top-4\",\n  \"bottom-left\": \"bottom-6 left-4\",\n  \"bottom-center\": \"bottom-6 left-1/2 -translate-x-1/2\",\n  \"bottom-right\": \"bottom-6 right-4\",\n};\n\nlet idSeed = 0;\n\nfunction createToast(input: ToastInput, defaultDuration: number): AnimatedToast {\n  return {\n    duration: defaultDuration,\n    dismissible: true,\n    ...input,\n    id: input.id ?? `toast-${Date.now()}-${idSeed++}`,\n    createdAt: Date.now(),\n  };\n}\n\ntype ToastPromiseObject = Omit<ToastInput, \"id\" | \"status\"> & { title: ReactNode };\n\nfunction isToastContentObject(content: ToastPromiseContent): content is ToastPromiseObject {\n  return (\n    typeof content === \"object\" &&\n    content !== null &&\n    !isValidElement(content) &&\n    !Array.isArray(content) &&\n    \"title\" in content\n  );\n}\n\nfunction resolvePromiseContent(content: ToastPromiseContent): ToastPromiseObject {\n  return isToastContentObject(content) ? content : { title: content as ReactNode };\n}\n\nexport function useAnimatedToastStack({\n  initialToasts = [],\n  defaultDuration = 4200,\n  limit,\n}: UseAnimatedToastStackOptions = {}) {\n  const toastTimers = useRef<Map<string, { timer: number; signature: string }>>(new Map());\n  const [toasts, setToasts] = useState<AnimatedToast[]>(() =>\n    initialToasts.map((toast) => createToast(toast, defaultDuration)),\n  );\n\n  const dismissToast = useCallback((id: string) => {\n    setToasts((current) => current.filter((toast) => toast.id !== id));\n  }, []);\n\n  const clearToasts = useCallback(() => {\n    setToasts([]);\n  }, []);\n\n  const showToast = useCallback(\n    (input: ToastInput) => {\n      const toast = createToast(input, defaultDuration);\n      setToasts((current) => {\n        const next = [...current, toast];\n        return typeof limit === \"number\" ? next.slice(-limit) : next;\n      });\n      return toast.id;\n    },\n    [defaultDuration, limit],\n  );\n\n  const updateToast = useCallback((id: string, patch: Partial<ToastInput>) => {\n    setToasts((current) =>\n      current.map((toast) =>\n        toast.id === id\n          ? {\n              ...toast,\n              ...patch,\n              id,\n              createdAt: patch.duration === undefined ? toast.createdAt : Date.now(),\n            }\n          : toast,\n      ),\n    );\n  }, []);\n\n  /**\n   * One toast that tracks a promise: a spinner while pending (no auto-dismiss),\n   * then morphs in place into success or error. Settling restarts the dismiss\n   * timer because `updateToast` re-stamps `createdAt` whenever a duration is set.\n   * Returns the toast id plus the original promise so callers can still await it.\n   */\n  const promiseToast = useCallback(\n    <T,>(\n      promise: Promise<T> | (() => Promise<T>),\n      messages: ToastPromiseMessages<T>,\n    ): ToastPromiseResult<T> => {\n      const settledDuration = messages.duration ?? defaultDuration;\n      const loading: ToastInput = {\n        ...resolvePromiseContent(messages.loading),\n        status: \"loading\",\n        duration: 0,\n      };\n\n      let id: string;\n      const reuseId = messages.id;\n      if (reuseId) {\n        id = reuseId;\n        setToasts((current) => {\n          if (current.some((toast) => toast.id === reuseId)) {\n            return current.map((toast) =>\n              toast.id === reuseId\n                ? {\n                    ...toast,\n                    description: undefined,\n                    action: undefined,\n                    icon: undefined,\n                    ...loading,\n                    id: reuseId,\n                  }\n                : toast,\n            );\n          }\n          const next = [...current, createToast({ ...loading, id: reuseId }, defaultDuration)];\n          return typeof limit === \"number\" ? next.slice(-limit) : next;\n        });\n      } else {\n        id = showToast(loading);\n      }\n\n      const settle = (status: \"success\" | \"error\", content: ToastPromiseContent) => {\n        const resolved = resolvePromiseContent(content);\n        updateToast(id, {\n          // Drop loading-phase extras the settled copy does not restate.\n          description: undefined,\n          action: undefined,\n          icon: undefined,\n          ...resolved,\n          status,\n          duration: resolved.duration ?? settledDuration,\n        });\n      };\n\n      let result: Promise<T>;\n      try {\n        result = typeof promise === \"function\" ? promise() : promise;\n      } catch (error) {\n        result = Promise.reject(error);\n      }\n\n      // Attaching both handlers here also keeps a rejection from surfacing as\n      // unhandled when the caller ignores `result`.\n      result.then(\n        (data) => {\n          const { success } = messages;\n          settle(\"success\", typeof success === \"function\" ? success(data) : success);\n        },\n        (error: unknown) => {\n          const { error: onError } = messages;\n          settle(\"error\", typeof onError === \"function\" ? onError(error) : onError);\n        },\n      );\n\n      return { id, result };\n    },\n    [defaultDuration, limit, showToast, updateToast],\n  );\n\n  useEffect(() => {\n    const activeIds = new Set(toasts.map((toast) => toast.id));\n\n    toastTimers.current.forEach((entry, id) => {\n      if (!activeIds.has(id)) {\n        window.clearTimeout(entry.timer);\n        toastTimers.current.delete(id);\n      }\n    });\n\n    toasts.forEach((toast) => {\n      const duration = toast.duration ?? defaultDuration;\n      const existing = toastTimers.current.get(toast.id);\n\n      if (duration <= 0) {\n        if (existing) {\n          window.clearTimeout(existing.timer);\n          toastTimers.current.delete(toast.id);\n        }\n        return;\n      }\n\n      const createdAt = toast.createdAt ?? Date.now();\n      const signature = `${createdAt}:${duration}`;\n\n      if (existing?.signature === signature) {\n        return;\n      }\n\n      if (existing) {\n        window.clearTimeout(existing.timer);\n      }\n\n      const elapsed = Date.now() - createdAt;\n      const remaining = Math.max(duration - elapsed, 0);\n      const timer = window.setTimeout(() => {\n        toastTimers.current.delete(toast.id);\n        dismissToast(toast.id);\n      }, remaining);\n\n      toastTimers.current.set(toast.id, { timer, signature });\n    });\n  }, [defaultDuration, dismissToast, toasts]);\n\n  useEffect(() => {\n    const timers = toastTimers.current;\n\n    return () => {\n      timers.forEach((entry) => {\n        window.clearTimeout(entry.timer);\n      });\n      timers.clear();\n    };\n  }, []);\n\n  return useMemo(\n    () => ({\n      toasts,\n      showToast,\n      updateToast,\n      promiseToast,\n      dismissToast,\n      clearToasts,\n      setToasts,\n    }),\n    [clearToasts, dismissToast, promiseToast, showToast, toasts, updateToast],\n  );\n}\n\nexport function AnimatedToastStack({\n  toasts,\n  onDismiss,\n  position = \"bottom-right\",\n  placement,\n  fixed = false,\n  portal,\n  portalRoot,\n  maxVisible = 4,\n  className,\n  classNames,\n  icons,\n  renderToast,\n}: AnimatedToastStackProps) {\n  const [portalTarget, setPortalTarget] = useState<Element | null>(null);\n  const visibleToasts = toasts.slice(-maxVisible);\n  const isBottom = position.startsWith(\"bottom\");\n  const resolvedPlacement = placement ?? (fixed ? \"fixed\" : \"static\");\n  const shouldPortal = portal ?? resolvedPlacement === \"fixed\";\n\n  useEffect(() => {\n    setPortalTarget(shouldPortal ? (portalRoot ?? document.body) : null);\n  }, [portalRoot, shouldPortal]);\n\n  const stack = (\n    <ol\n      aria-live=\"polite\"\n      aria-atomic=\"false\"\n      className={cn(\n        \"pointer-events-none flex w-[calc(100vw-2rem)] max-w-sm gap-2\",\n        isBottom ? \"flex-col-reverse\" : \"flex-col\",\n        resolvedPlacement === \"fixed\" && \"fixed z-[90]\",\n        resolvedPlacement === \"absolute\" && \"absolute z-20\",\n        resolvedPlacement !== \"static\" && POSITION_CLASS[position],\n        classNames?.root,\n        className,\n      )}\n    >\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        {visibleToasts.map((toast, index) => (\n          <ToastItem\n            key={toast.id}\n            toast={toast}\n            index={index}\n            onDismiss={onDismiss}\n            classNames={classNames}\n            icons={icons}\n            renderToast={renderToast}\n          />\n        ))}\n      </AnimatePresence>\n    </ol>\n  );\n\n  if (shouldPortal && !portalTarget) {\n    return null;\n  }\n\n  if (shouldPortal && portalTarget) {\n    return createPortal(stack, portalTarget);\n  }\n\n  return stack;\n}\n\nconst ToastItem = memo(function ToastItem({\n  toast,\n  index,\n  onDismiss,\n  classNames,\n  icons,\n  renderToast,\n}: {\n  toast: AnimatedToast;\n  index: number;\n  onDismiss?: (id: string) => void;\n  classNames?: ToastClassNames;\n  icons?: Partial<Record<ToastStatus, ReactNode>>;\n  renderToast?: (toast: AnimatedToast) => ReactNode;\n}) {\n  const reduce = useReducedMotion();\n  const status = toast.status ?? \"neutral\";\n  const Icon = STATUS_ICON[status];\n  const iconNode = icons?.[status] ?? toast.icon ?? <Icon className=\"h-3.5 w-3.5\" />;\n  const canDismiss = toast.dismissible !== false && Boolean(onDismiss);\n\n  return (\n    <motion.li\n      layout\n      initial={\n        reduce\n          ? { opacity: 0 }\n          : { opacity: 0, y: 22, scale: 0.96, filter: \"blur(10px)\" }\n      }\n      animate={\n        reduce\n          ? { opacity: 1 }\n          : { opacity: 1, y: 0, scale: 1, filter: \"blur(0px)\" }\n      }\n      exit={\n        reduce\n          ? { opacity: 0 }\n          : {\n              opacity: 0,\n              x: 32,\n              scale: 0.96,\n              filter: \"blur(8px)\",\n              transition: { duration: 0.18, ease: EASE_OUT },\n            }\n      }\n      transition={STACK_SPRING}\n      drag={canDismiss && !reduce ? \"x\" : false}\n      dragConstraints={{ left: 0, right: 0 }}\n      dragElastic={0.18}\n      onDragEnd={(_, info) => {\n        if (!canDismiss || !onDismiss) return;\n        if (Math.abs(info.offset.x) > 72 || Math.abs(info.velocity.x) > 520) {\n          onDismiss(toast.id);\n        }\n      }}\n      className={cn(\"pointer-events-auto relative will-change-transform\", classNames?.item)}\n      style={{ zIndex: 20 - index }}\n    >\n      <div\n        className={cn(\n          \"relative overflow-hidden rounded-2xl border border-border bg-card/95 p-3 shadow-2xl backdrop-blur-xl\",\n          classNames?.surface,\n        )}\n      >\n        {renderToast ? (\n          renderToast(toast)\n        ) : (\n          <div className=\"flex items-start gap-3\">\n            <motion.span\n              layout\n              className={cn(\n                \"mt-0.5 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full\",\n                STATUS_CLASS[status],\n                classNames?.iconWrap,\n              )}\n            >\n              <AnimatePresence mode=\"popLayout\" initial={false}>\n                <motion.span\n                  key={status}\n                  initial={\n                    reduce\n                      ? { opacity: 0 }\n                      : { opacity: 0, y: 8, scale: 0.8, filter: \"blur(6px)\" }\n                  }\n                  animate={\n                    reduce\n                      ? { opacity: 1 }\n                      : { opacity: 1, y: 0, scale: 1, filter: \"blur(0px)\" }\n                  }\n                  exit={\n                    reduce\n                      ? { opacity: 0 }\n                      : { opacity: 0, y: -8, scale: 0.9, filter: \"blur(6px)\" }\n                  }\n                  transition={CONTENT_TRANSITION}\n                  className=\"inline-flex\"\n                >\n                  {status === \"loading\" ? (\n                    <span className=\"inline-flex animate-spin\">{iconNode}</span>\n                  ) : (\n                    iconNode\n                  )}\n                </motion.span>\n              </AnimatePresence>\n            </motion.span>\n\n            {/* layout=\"position\" keeps the copy from stretching while the item's\n                height morphs between phases (e.g. a promise toast settling). */}\n            <motion.div layout=\"position\" className={cn(\"min-w-0 flex-1\", classNames?.content)}>\n              <AnimatePresence mode=\"popLayout\" initial={false}>\n                <motion.div\n                  key={`${toast.id}-${status}-${String(toast.title)}`}\n                  initial={\n                    reduce\n                      ? { opacity: 0 }\n                      : { opacity: 0, y: 8, filter: \"blur(6px)\" }\n                  }\n                  animate={\n                    reduce\n                      ? { opacity: 1 }\n                      : { opacity: 1, y: 0, filter: \"blur(0px)\" }\n                  }\n                  exit={\n                    reduce\n                      ? { opacity: 0 }\n                      : { opacity: 0, y: -8, filter: \"blur(6px)\" }\n                  }\n                  transition={CONTENT_TRANSITION}\n                >\n                  <p\n                    className={cn(\n                      \"truncate text-sm font-medium leading-5 text-foreground\",\n                      classNames?.title,\n                    )}\n                  >\n                    {toast.title}\n                  </p>\n                  {toast.description ? (\n                    <p\n                      className={cn(\n                        \"mt-0.5 line-clamp-2 text-xs leading-4 text-muted-foreground\",\n                        classNames?.description,\n                      )}\n                    >\n                      {toast.description}\n                    </p>\n                  ) : null}\n                </motion.div>\n              </AnimatePresence>\n\n              {toast.action ? (\n                <button\n                  type=\"button\"\n                  onClick={() => toast.action?.onClick(toast)}\n                  className={cn(\n                    \"mt-2 inline-flex h-7 items-center rounded-full bg-primary/[0.06] px-3 text-xs font-medium text-foreground transition-colors hover:bg-primary/[0.1]\",\n                    classNames?.action,\n                  )}\n                >\n                  {toast.action.label}\n                </button>\n              ) : null}\n            </motion.div>\n\n            {canDismiss ? (\n              <button\n                type=\"button\"\n                onClick={() => onDismiss?.(toast.id)}\n                aria-label=\"Dismiss toast\"\n                className={cn(\n                  \"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-primary/[0.06] hover:text-foreground\",\n                  classNames?.close,\n                )}\n              >\n                <X className=\"h-3.5 w-3.5\" />\n              </button>\n            ) : null}\n          </div>\n        )}\n\n      </div>\n    </motion.li>\n  );\n});\n"},{"path":"components/motion/button/index.tsx","type":"registry:component","target":"@components/motion/button/index.tsx","content":"export type {\n  ButtonLinkProps,\n  ButtonProps,\n  ButtonSize,\n  ButtonVariant,\n} from \"./base\";\nexport { Button, ButtonLink } from \"./base\";\nexport type { MagneticButtonProps } from \"./magnetic\";\nexport { MagneticButton } from \"./magnetic\";\nexport type { ButtonState, StatefulButtonProps } from \"./stateful\";\nexport { StatefulButton } from \"./stateful\";\n"},{"path":"components/motion/checkbox.tsx","type":"registry:component","target":"@components/motion/checkbox.tsx","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useId } from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { TOUCH_TARGET_CLASS } from \"@/lib/touch\";\nimport { cn } from \"@/lib/utils\";\n\nconst CHECK_PATH = \"M5 13l4 4L19 7\";\nconst INDETERMINATE_PATH = \"M6 12h12\";\n\nexport interface CheckboxProps {\n  checked: boolean;\n  onCheckedChange: (checked: boolean) => void;\n  disabled?: boolean;\n  indeterminate?: boolean;\n  label?: string;\n  className?: string;\n  id?: string;\n  \"aria-label\"?: string;\n  /** Associates an external message (e.g. a form error) with the control. */\n  \"aria-describedby\"?: string;\n}\n\nexport function Checkbox({\n  checked,\n  onCheckedChange,\n  disabled,\n  indeterminate,\n  label,\n  className,\n  id: idProp,\n  \"aria-label\": ariaLabel,\n  \"aria-describedby\": ariaDescribedBy,\n}: CheckboxProps) {\n  const autoId = useId();\n  const id = idProp ?? autoId;\n  const reduce = useReducedMotion();\n  const showMark = checked || indeterminate;\n  const path = indeterminate ? INDETERMINATE_PATH : CHECK_PATH;\n\n  return (\n    <label\n      htmlFor={id}\n      className={cn(\n        \"inline-flex items-center gap-3\",\n        disabled ? \"cursor-not-allowed\" : \"cursor-pointer\",\n        className,\n      )}\n    >\n      <motion.button\n        id={id}\n        type=\"button\"\n        role=\"checkbox\"\n        aria-checked={indeterminate ? \"mixed\" : checked}\n        aria-label={ariaLabel}\n        aria-describedby={ariaDescribedBy}\n        disabled={disabled}\n        onClick={() => !disabled && onCheckedChange(!checked)}\n        whileTap={reduce || disabled ? undefined : { scale: 0.92 }}\n        transition={SPRING_PRESS}\n        data-state={\n          checked ? \"checked\" : indeterminate ? \"indeterminate\" : \"unchecked\"\n        }\n        className={cn(\n          \"relative inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md border-2 outline-none transition-colors duration-200\",\n          TOUCH_TARGET_CLASS,\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          \"disabled:cursor-not-allowed disabled:opacity-60\",\n          showMark\n            ? \"border-primary bg-primary text-primary-foreground\"\n            : \"border-muted-foreground/50 bg-background hover:border-muted-foreground\",\n        )}\n      >\n        <AnimatePresence initial={false}>\n          {showMark ? (\n            <motion.svg\n              key={indeterminate ? \"indeterminate\" : \"checked\"}\n              width=\"12\"\n              height=\"12\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth={3}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduce ? { opacity: 1 } : { opacity: 0, scale: 0.5 }}\n              animate={reduce ? { opacity: 1 } : { opacity: 1, scale: 1 }}\n              exit={\n                reduce\n                  ? { opacity: 0 }\n                  : { opacity: 0, scale: 0.5, filter: \"blur(4px)\" }\n              }\n              transition={\n                reduce ? { duration: 0 } : { duration: 0.16, ease: EASE_OUT }\n              }\n              aria-hidden\n            >\n              <title>{indeterminate ? \"Partially selected\" : \"Selected\"}</title>\n              <motion.path\n                d={path}\n                initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}\n                animate={{ pathLength: 1 }}\n                transition={\n                  reduce\n                    ? { duration: 0 }\n                    : {\n                        duration: indeterminate ? 0.2 : 0.3,\n                        ease: EASE_OUT,\n                        delay: 0.04,\n                      }\n                }\n              />\n            </motion.svg>\n          ) : null}\n        </AnimatePresence>\n      </motion.button>\n      {label ? (\n        <span className={cn(\"select-none text-sm text-foreground\", disabled && \"opacity-60\")}>\n          {label}\n        </span>\n      ) : null}\n    </label>\n  );\n}\n"},{"path":"components/motion/dialog.tsx","type":"registry:component","target":"@components/motion/dialog.tsx","content":"\"use client\";\n\nimport { X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useIsPresent,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  type ComponentPropsWithoutRef,\n  type MouseEvent,\n  type ReactElement,\n  type ReactNode,\n  type RefObject,\n  cloneElement,\n  createContext,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Button, type ButtonProps } from \"@/components/motion/button\";\nimport { Input, type InputProps } from \"@/components/motion/input\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\n/* -------------------------------------------------------------------------- */\n/*                               Shared plumbing                              */\n/* -------------------------------------------------------------------------- */\n\nconst FOCUSABLE_SELECTOR = [\n  \"a[href]\",\n  \"button:not([disabled])\",\n  \"input:not([disabled]):not([type='hidden'])\",\n  \"select:not([disabled])\",\n  \"textarea:not([disabled])\",\n  \"[contenteditable='true']\",\n  '[tabindex]:not([tabindex=\"-1\"])',\n].join(\",\");\n\nfunction getFocusable(root: HTMLElement | null) {\n  if (!root) return [];\n  return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(\n    (el) => el.tabIndex >= 0 && !el.closest(\"[inert]\") && el.getClientRects().length > 0,\n  );\n}\n\n// Open dialogs, innermost last. Only the top one answers Escape and Tab, so an\n// AlertDialog opened from inside a Dialog doesn't close both at once.\nconst dialogStack: string[] = [];\n\n// Reference-counted body scroll lock. The scrollbar's width is put back as\n// padding so the page behind doesn't jump sideways when the bar disappears.\nlet lockCount = 0;\nlet savedBodyStyle: { overflow: string; paddingRight: string } | null = null;\n\nfunction lockScroll() {\n  lockCount += 1;\n  if (lockCount > 1) return;\n  const body = document.body;\n  const scrollbar = window.innerWidth - document.documentElement.clientWidth;\n  savedBodyStyle = {\n    overflow: body.style.overflow,\n    paddingRight: body.style.paddingRight,\n  };\n  if (scrollbar > 0) {\n    const current = Number.parseFloat(getComputedStyle(body).paddingRight) || 0;\n    body.style.paddingRight = `${current + scrollbar}px`;\n  }\n  body.style.overflow = \"hidden\";\n}\n\nfunction unlockScroll() {\n  lockCount = Math.max(0, lockCount - 1);\n  if (lockCount > 0 || !savedBodyStyle) return;\n  document.body.style.overflow = savedBodyStyle.overflow;\n  document.body.style.paddingRight = savedBodyStyle.paddingRight;\n  savedBodyStyle = null;\n}\n\nconst SM_QUERY = \"(max-width: 639.98px)\";\n\nfunction subscribeSmall(callback: () => void) {\n  const mql = window.matchMedia(SM_QUERY);\n  mql.addEventListener(\"change\", callback);\n  return () => mql.removeEventListener(\"change\", callback);\n}\n\n/** True below Tailwind's `sm` breakpoint. Always false on the server. */\nfunction useBelowSm() {\n  return useSyncExternalStore(\n    subscribeSmall,\n    () => window.matchMedia(SM_QUERY).matches,\n    () => false,\n  );\n}\n\nconst SHAKE_X = [0, -7, 7, -5, 5, -2, 0];\n\n/* -------------------------------------------------------------------------- */\n/*                                   Context                                  */\n/* -------------------------------------------------------------------------- */\n\ntype DialogContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  alert: boolean;\n  contentId: string;\n  titleId: string;\n  descriptionId: string;\n  hasDescription: boolean;\n  setHasDescription: (value: boolean) => void;\n  /** Type-to-confirm (AlertDialog only). */\n  confirmText?: string;\n  confirmValue: string;\n  setConfirmValue: (value: string) => void;\n  confirmValid: boolean;\n  confirmAttempted: boolean;\n  setConfirmAttempted: (value: boolean) => void;\n  confirmFieldRef: RefObject<HTMLDivElement | null>;\n  confirmInputRef: RefObject<HTMLInputElement | null>;\n};\n\nconst DialogContext = createContext<DialogContextValue | null>(null);\n\nfunction useDialogContext(component: string) {\n  const context = useContext(DialogContext);\n  if (!context) {\n    throw new Error(`${component} must be used within <Dialog> or <AlertDialog>`);\n  }\n  return context;\n}\n\nexport interface DialogProps {\n  children: ReactNode;\n  /** Controlled open state. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nfunction DialogRoot({\n  children,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  alert,\n  confirmText,\n}: DialogProps & { alert: boolean; confirmText?: string }) {\n  const id = useId();\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const controlled = openProp !== undefined;\n  const open = controlled ? openProp : internalOpen;\n  const [hasDescription, setHasDescription] = useState(false);\n  const [confirmValue, setConfirmValue] = useState(\"\");\n  const [confirmAttempted, setConfirmAttempted] = useState(false);\n  const confirmFieldRef = useRef<HTMLDivElement>(null);\n  const confirmInputRef = useRef<HTMLInputElement>(null);\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!controlled) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [controlled, onOpenChange],\n  );\n\n  // A fresh confirmation every time the dialog opens.\n  useEffect(() => {\n    if (!open) return;\n    setConfirmValue(\"\");\n    setConfirmAttempted(false);\n  }, [open]);\n\n  const confirmValid =\n    confirmText === undefined || confirmValue.trim() === confirmText;\n\n  const value = useMemo<DialogContextValue>(\n    () => ({\n      open,\n      setOpen,\n      alert,\n      contentId: `${id}-content`,\n      titleId: `${id}-title`,\n      descriptionId: `${id}-description`,\n      hasDescription,\n      setHasDescription,\n      confirmText,\n      confirmValue,\n      setConfirmValue,\n      confirmValid,\n      confirmAttempted,\n      setConfirmAttempted,\n      confirmFieldRef,\n      confirmInputRef,\n    }),\n    [\n      open,\n      setOpen,\n      alert,\n      id,\n      hasDescription,\n      confirmText,\n      confirmValue,\n      confirmValid,\n      confirmAttempted,\n    ],\n  );\n\n  return <DialogContext.Provider value={value}>{children}</DialogContext.Provider>;\n}\n\n/**\n * Modal dialog root. Holds open state (controlled via `open`/`onOpenChange`\n * or uncontrolled via `defaultOpen`) for its Trigger, Content and Close parts.\n */\nexport function Dialog(props: DialogProps) {\n  return <DialogRoot {...props} alert={false} />;\n}\n\nexport interface AlertDialogProps extends DialogProps {\n  /**\n   * Require the user to type this exact text (e.g. the project name) into\n   * `AlertDialogConfirmInput` before `AlertDialogAction` goes through.\n   */\n  confirmText?: string;\n}\n\n/**\n * Interrupting confirmation. Renders `role=\"alertdialog\"`, never closes on an\n * outside click, and focuses `AlertDialogCancel` first so a stray Enter can't\n * confirm a destructive action.\n */\nexport function AlertDialog({ confirmText, ...props }: AlertDialogProps) {\n  return <DialogRoot {...props} alert confirmText={confirmText} />;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                               Trigger & Close                              */\n/* -------------------------------------------------------------------------- */\n\ntype ClonableChild = ReactElement<Record<string, unknown>>;\n\nfunction composeClick(\n  child: ClonableChild,\n  action: () => void,\n): (event: MouseEvent<HTMLElement>) => void {\n  const childOnClick = child.props.onClick as\n    | ((event: MouseEvent<HTMLElement>) => void)\n    | undefined;\n  return (event) => {\n    childOnClick?.(event);\n    if (!event.defaultPrevented) action();\n  };\n}\n\nexport interface DialogTriggerProps extends ButtonProps {\n  /** Merge behaviour onto the single child element instead of rendering a `Button`. */\n  asChild?: boolean;\n}\n\n/** Opens the dialog. Renders a library `Button`, or wraps its child with `asChild`. */\nexport const DialogTrigger = forwardRef<HTMLButtonElement, DialogTriggerProps>(\n  function DialogTrigger({ asChild, children, onClick, ...rest }, ref) {\n    const context = useDialogContext(\"DialogTrigger\");\n    const aria = {\n      \"aria-haspopup\": \"dialog\" as const,\n      \"aria-expanded\": context.open,\n      \"aria-controls\": context.open ? context.contentId : undefined,\n    };\n\n    if (asChild && isValidElement(children)) {\n      const child = children as ClonableChild;\n      return cloneElement(child, {\n        ...aria,\n        onClick: composeClick(child, () => context.setOpen(true)),\n      });\n    }\n\n    return (\n      <Button\n        ref={ref}\n        {...rest}\n        {...aria}\n        onClick={(event) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) context.setOpen(true);\n        }}\n      >\n        {children}\n      </Button>\n    );\n  },\n);\n\nexport interface DialogCloseProps extends ButtonProps {\n  /** Merge behaviour onto the single child element instead of rendering a `Button`. */\n  asChild?: boolean;\n}\n\n/** Closes the dialog. Renders a library `Button` (outline by default), or wraps its child. */\nexport const DialogClose = forwardRef<HTMLButtonElement, DialogCloseProps>(\n  function DialogClose(\n    { asChild, children, onClick, variant = \"outline\", ...rest },\n    ref,\n  ) {\n    const context = useDialogContext(\"DialogClose\");\n\n    if (asChild && isValidElement(children)) {\n      const child = children as ClonableChild;\n      return cloneElement(child, {\n        onClick: composeClick(child, () => context.setOpen(false)),\n      });\n    }\n\n    return (\n      <Button\n        ref={ref}\n        variant={variant}\n        {...rest}\n        onClick={(event) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) context.setOpen(false);\n        }}\n      >\n        {children}\n      </Button>\n    );\n  },\n);\n\n/* -------------------------------------------------------------------------- */\n/*                                   Content                                  */\n/* -------------------------------------------------------------------------- */\n\nexport interface DialogContentProps {\n  children: ReactNode;\n  className?: string;\n  /** Class for the dimmed, blurred backdrop. */\n  overlayClassName?: string;\n  /** Show the corner close (X) button. Default true for Dialog, false for AlertDialog. */\n  showCloseButton?: boolean;\n  closeLabel?: string;\n  /**\n   * Close when the backdrop is pressed. Default true for Dialog; AlertDialog\n   * ignores it and never closes on an outside press.\n   */\n  closeOnOutsideClick?: boolean;\n  /** Element to focus on open. Falls back to `[data-dialog-autofocus]`, then the first focusable. */\n  initialFocus?: RefObject<HTMLElement | null>;\n  /** Become a bottom-anchored sheet below the `sm` breakpoint. Default true. */\n  sheetOnMobile?: boolean;\n  /** Accessible name, when the content has no `DialogTitle`. */\n  \"aria-label\"?: string;\n}\n\nconst ENTER = { duration: 0.22, ease: EASE_OUT } as const;\nconst EXIT = { duration: 0.14, ease: EASE_OUT } as const;\n\nfunction DialogContentInner({\n  children,\n  className,\n  overlayClassName,\n  showCloseButton,\n  closeLabel = \"Close\",\n  closeOnOutsideClick = true,\n  initialFocus,\n  sheetOnMobile = true,\n  \"aria-label\": ariaLabel,\n}: DialogContentProps) {\n  const context = useDialogContext(\"DialogContent\");\n  const reduce = useReducedMotion() ?? false;\n  const isPresent = useIsPresent();\n  const belowSm = useBelowSm();\n  const sheet = sheetOnMobile && belowSm;\n  const panelRef = useRef<HTMLDivElement>(null);\n  const stackId = useId();\n  const { setOpen } = context;\n  // Read through a ref so a new onOpenChange identity (inline handlers) does\n  // not re-run the focus/scroll-lock effect and yank focus mid-typing.\n  const setOpenRef = useRef(setOpen);\n  const initialFocusRef = useRef(initialFocus);\n  useEffect(() => {\n    setOpenRef.current = setOpen;\n    initialFocusRef.current = initialFocus;\n  });\n  const dismissOutside = closeOnOutsideClick && !context.alert;\n  const withClose = showCloseButton ?? !context.alert;\n\n  // Scroll lock, stack registration, initial focus, focus trap, Escape and\n  // focus return. Runs while this panel is mounted *and* present, so the\n  // exit animation already behaves like a closed dialog.\n  useEffect(() => {\n    if (!isPresent) return;\n    const returnTo =\n      document.activeElement instanceof HTMLElement ? document.activeElement : null;\n    dialogStack.push(stackId);\n    lockScroll();\n\n    const frame = requestAnimationFrame(() => {\n      const panel = panelRef.current;\n      if (!panel) return;\n      const target =\n        initialFocusRef.current?.current ??\n        panel.querySelector<HTMLElement>(\"[data-dialog-autofocus]\") ??\n        getFocusable(panel).find((el) => !el.hasAttribute(\"data-dialog-close-x\")) ??\n        panel;\n      target.focus({ preventScroll: true });\n    });\n\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (dialogStack[dialogStack.length - 1] !== stackId) return;\n      if (event.key === \"Escape\") {\n        event.preventDefault();\n        setOpenRef.current(false);\n        return;\n      }\n      if (event.key !== \"Tab\") return;\n      const panel = panelRef.current;\n      const focusable = getFocusable(panel);\n      if (focusable.length === 0) {\n        event.preventDefault();\n        panel?.focus();\n        return;\n      }\n      const first = focusable[0];\n      const last = focusable[focusable.length - 1];\n      const active = document.activeElement;\n      const outside = !panel?.contains(active);\n      if (event.shiftKey && (active === first || active === panel || outside)) {\n        event.preventDefault();\n        last.focus();\n      } else if (!event.shiftKey && (active === last || outside)) {\n        event.preventDefault();\n        first.focus();\n      }\n    };\n\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => {\n      cancelAnimationFrame(frame);\n      document.removeEventListener(\"keydown\", onKeyDown);\n      const index = dialogStack.lastIndexOf(stackId);\n      if (index !== -1) dialogStack.splice(index, 1);\n      unlockScroll();\n      if (returnTo?.isConnected) returnTo.focus({ preventScroll: true });\n    };\n  }, [isPresent, stackId]);\n\n  const panelInitial = reduce\n    ? { opacity: 0 }\n    : sheet\n      ? { opacity: 0, y: \"100%\" }\n      : { opacity: 0, scale: 0.96 };\n  const panelExit = reduce\n    ? { opacity: 0, transition: EXIT }\n    : sheet\n      ? { opacity: 0, y: \"100%\", transition: { duration: 0.2, ease: EASE_OUT } }\n      : { opacity: 0, scale: 0.97, transition: EXIT };\n\n  return (\n    <div\n      className=\"pointer-events-none fixed inset-0 z-[100]\"\n      style={{ pointerEvents: isPresent ? undefined : \"none\" }}\n    >\n      <motion.button\n        type=\"button\"\n        aria-label={closeLabel}\n        tabIndex={-1}\n        disabled={!dismissOutside}\n        onClick={() => setOpen(false)}\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        exit={{ opacity: 0, transition: EXIT }}\n        transition={ENTER}\n        className={cn(\n          \"pointer-events-auto absolute inset-0 h-full w-full cursor-default bg-black/35 backdrop-blur-sm disabled:cursor-default dark:bg-black/55\",\n          overlayClassName,\n        )}\n      />\n\n      <div\n        className={cn(\n          \"pointer-events-none absolute inset-0 flex justify-center\",\n          sheet ? \"items-end\" : \"items-center p-4\",\n        )}\n      >\n        <motion.div\n          ref={panelRef}\n          id={context.contentId}\n          role={context.alert ? \"alertdialog\" : \"dialog\"}\n          aria-modal=\"true\"\n          aria-label={ariaLabel}\n          aria-labelledby={ariaLabel ? undefined : context.titleId}\n          aria-describedby={context.hasDescription ? context.descriptionId : undefined}\n          tabIndex={-1}\n          initial={panelInitial}\n          animate={{ opacity: 1, scale: 1, y: 0 }}\n          exit={panelExit}\n          transition={reduce ? ENTER : sheet ? { duration: 0.32, ease: EASE_OUT } : ENTER}\n          style={{ pointerEvents: isPresent ? \"auto\" : \"none\" }}\n          className={cn(\n            \"relative flex w-full flex-col gap-5 overflow-y-auto border-border bg-background p-6 text-foreground shadow-2xl outline-none\",\n            \"[scrollbar-color:var(--border-strong)_transparent] [scrollbar-width:thin]\",\n            sheet\n              ? \"max-h-[88dvh] rounded-t-3xl border-t pb-[max(1.5rem,env(safe-area-inset-bottom))]\"\n              : \"max-h-[calc(100dvh-2rem)] max-w-md rounded-3xl border\",\n            className,\n          )}\n        >\n          {sheet ? (\n            <span\n              aria-hidden=\"true\"\n              className=\"mx-auto -mt-2 -mb-1 h-1 w-10 shrink-0 rounded-full bg-foreground/15\"\n            />\n          ) : null}\n          {children}\n          {withClose ? (\n            <button\n              type=\"button\"\n              aria-label={closeLabel}\n              data-dialog-close-x=\"\"\n              onClick={() => setOpen(false)}\n              className=\"absolute top-4 right-4 inline-flex size-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-foreground/[0.06] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n            >\n              <X className=\"size-4\" aria-hidden=\"true\" />\n            </button>\n          ) : null}\n        </motion.div>\n      </div>\n    </div>\n  );\n}\n\n/**\n * The modal surface. Portals to `document.body`, fades a blurred backdrop in,\n * scales the panel from 0.96 (fade only under reduced motion), traps focus,\n * closes on Escape and returns focus to whatever opened it.\n */\nexport function DialogContent(props: DialogContentProps) {\n  const context = useDialogContext(\"DialogContent\");\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  if (!mounted) return null;\n  return createPortal(\n    <AnimatePresence>\n      {context.open ? <DialogContentInner key=\"dialog\" {...props} /> : null}\n    </AnimatePresence>,\n    document.body,\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                Text & layout                               */\n/* -------------------------------------------------------------------------- */\n\nexport function DialogHeader({\n  className,\n  ...props\n}: ComponentPropsWithoutRef<\"div\">) {\n  return (\n    <div\n      className={cn(\"flex flex-col gap-1.5 pr-8 text-left\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DialogFooter({\n  className,\n  ...props\n}: ComponentPropsWithoutRef<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function DialogTitle({\n  className,\n  ...props\n}: Omit<ComponentPropsWithoutRef<\"h2\">, \"id\">) {\n  const context = useDialogContext(\"DialogTitle\");\n  return (\n    <h2\n      id={context.titleId}\n      className={cn(\"text-lg font-semibold tracking-tight\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DialogDescription({\n  className,\n  ...props\n}: Omit<ComponentPropsWithoutRef<\"p\">, \"id\">) {\n  const context = useDialogContext(\"DialogDescription\");\n  const { setHasDescription } = context;\n  useEffect(() => {\n    setHasDescription(true);\n    return () => setHasDescription(false);\n  }, [setHasDescription]);\n  return (\n    <p\n      id={context.descriptionId}\n      className={cn(\"text-sm leading-relaxed text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                              AlertDialog parts                             */\n/* -------------------------------------------------------------------------- */\n\nexport const AlertDialogTrigger = DialogTrigger;\nexport type AlertDialogTriggerProps = DialogTriggerProps;\nexport const AlertDialogHeader = DialogHeader;\nexport const AlertDialogFooter = DialogFooter;\nexport const AlertDialogTitle = DialogTitle;\nexport const AlertDialogDescription = DialogDescription;\n\nexport type AlertDialogContentProps = Omit<\n  DialogContentProps,\n  \"closeOnOutsideClick\"\n>;\n\nexport function AlertDialogContent(props: AlertDialogContentProps) {\n  return <DialogContent {...props} />;\n}\n\nexport interface AlertDialogCancelProps extends ButtonProps {}\n\n/** The safe choice. Receives initial focus when the AlertDialog opens. */\nexport const AlertDialogCancel = forwardRef<HTMLButtonElement, AlertDialogCancelProps>(\n  function AlertDialogCancel({ children = \"Cancel\", ...props }, ref) {\n    return (\n      <DialogClose ref={ref} data-dialog-autofocus=\"\" {...props}>\n        {children}\n      </DialogClose>\n    );\n  },\n);\n\nexport interface AlertDialogActionProps extends ButtonProps {\n  /** `destructive` (default) paints the button in the destructive token. */\n  tone?: \"destructive\" | \"default\";\n}\n\n/**\n * The confirming action. Closes the dialog after `onClick` unless the handler\n * calls `event.preventDefault()`. With `confirmText` set on the AlertDialog it\n * stays `aria-disabled` until the text matches; pressing it early shakes the\n * confirm field and moves focus there.\n */\nexport const AlertDialogAction = forwardRef<HTMLButtonElement, AlertDialogActionProps>(\n  function AlertDialogAction(\n    { tone = \"destructive\", className, onClick, children, ...props },\n    ref,\n  ) {\n    const context = useDialogContext(\"AlertDialogAction\");\n    const reduce = useReducedMotion();\n    const blocked = !context.confirmValid;\n\n    return (\n      <Button\n        ref={ref}\n        variant=\"primary\"\n        aria-disabled={blocked || undefined}\n        data-alert-dialog-action=\"\"\n        {...props}\n        className={cn(\n          tone === \"destructive\" &&\n            \"bg-destructive text-white hover:bg-destructive/90\",\n          \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          blocked && \"opacity-55\",\n          blocked &&\n            (tone === \"destructive\" ? \"hover:bg-destructive\" : \"hover:bg-primary\"),\n          className,\n        )}\n        onClick={(event) => {\n          if (blocked) {\n            event.preventDefault();\n            // The field's own error shake plays the first time the message\n            // appears; later attempts shake it here instead.\n            const errorShowing = context.confirmAttempted;\n            context.setConfirmAttempted(true);\n            const field = context.confirmFieldRef.current;\n            if (field && errorShowing && !reduce) {\n              animate(field, { x: SHAKE_X }, { duration: 0.4 });\n            }\n            context.confirmInputRef.current?.focus();\n            return;\n          }\n          onClick?.(event);\n          if (!event.defaultPrevented) context.setOpen(false);\n        }}\n      >\n        {children}\n      </Button>\n    );\n  },\n);\n\nexport interface AlertDialogConfirmInputProps\n  extends Omit<InputProps, \"value\" | \"defaultValue\" | \"onChange\" | \"error\"> {\n  /** Message shown when confirming with a mismatch. */\n  mismatchMessage?: string;\n}\n\n/**\n * Type-to-confirm field for an `AlertDialog` with `confirmText`. Enter submits\n * through `AlertDialogAction`'s rules.\n */\nexport function AlertDialogConfirmInput({\n  label,\n  mismatchMessage = \"That doesn't match. Check the spelling and try again.\",\n  className,\n  onKeyDown,\n  ...props\n}: AlertDialogConfirmInputProps) {\n  const context = useDialogContext(\"AlertDialogConfirmInput\");\n  const target = context.confirmText ?? \"\";\n  const showError = context.confirmAttempted && !context.confirmValid;\n\n  return (\n    <div ref={context.confirmFieldRef} className={className}>\n      <Input\n        ref={context.confirmInputRef}\n        label={label ?? `Type \"${target}\" to confirm`}\n        autoComplete=\"off\"\n        autoCapitalize=\"off\"\n        spellCheck={false}\n        {...props}\n        value={context.confirmValue}\n        onChange={context.setConfirmValue}\n        error={showError ? mismatchMessage : undefined}\n        success={target !== \"\" && context.confirmValid}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (event.defaultPrevented || event.key !== \"Enter\") return;\n          event.preventDefault();\n          const panel = event.currentTarget.closest<HTMLElement>(\"[role='alertdialog']\");\n          panel?.querySelector<HTMLButtonElement>(\"[data-alert-dialog-action]\")?.click();\n        }}\n      />\n    </div>\n  );\n}\n"},{"path":"components/motion/empty-state.tsx","type":"registry:component","target":"@components/motion/empty-state.tsx","content":"\"use client\";\n\nimport { motion, useReducedMotion, type Variants } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { EASE_IN_OUT, EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type EmptyStateSize = \"sm\" | \"md\" | \"lg\";\nexport type EmptyStateIllustrationVariant = \"documents\" | \"search\";\n\n/* -------------------------------------------------------------------------- */\n/*                                Illustration                                */\n/* -------------------------------------------------------------------------- */\n\nexport interface EmptyStateIllustrationProps {\n  variant?: EmptyStateIllustrationVariant;\n  /** Width in px; height follows the 5:4 viewBox. */\n  size?: number;\n  className?: string;\n}\n\nconst draw = (delay: number, reduce: boolean) => ({\n  initial: reduce ? { pathLength: 1, opacity: 0 } : { pathLength: 0, opacity: 0 },\n  whileInView: { pathLength: 1, opacity: 1 },\n  viewport: { once: true },\n  transition: reduce\n    ? { duration: 0.2 }\n    : {\n        pathLength: { duration: 0.5, ease: EASE_OUT, delay },\n        opacity: { duration: 0.1, delay },\n      },\n});\n\n// Idle float: translate only (no rotate/scale), so no SVG transform-origin is\n// involved. Rotations in the art are static `transform` attributes on an\n// inner <g> that motion never touches.\nconst float = (reduce: boolean, amplitude: number, duration: number, delay: number) =>\n  reduce\n    ? {}\n    : {\n        animate: { y: [0, -amplitude, 0] },\n        transition: { duration, ease: EASE_IN_OUT, repeat: Infinity, delay },\n      };\n\n/** Small default empty-state art: a floating document stack or a search lens. */\nexport function EmptyStateIllustration({\n  variant = \"documents\",\n  size = 120,\n  className,\n}: EmptyStateIllustrationProps) {\n  const reduce = useReducedMotion() ?? false;\n  const lineClass = \"stroke-border-strong\";\n\n  return (\n    <svg\n      width={size}\n      height={(size * 4) / 5}\n      viewBox=\"0 0 120 96\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      className={cn(\"overflow-visible\", className)}\n    >\n      <ellipse cx=\"60\" cy=\"88\" rx=\"30\" ry=\"3.5\" className=\"fill-muted\" />\n      {variant === \"documents\" ? (\n        <>\n          <motion.g {...float(reduce, 2, 3.4, 0.4)}>\n            <g transform=\"rotate(-9 46 48)\">\n              <rect x=\"24\" y=\"20\" width=\"44\" height=\"56\" rx=\"7\" strokeWidth=\"1.5\" className=\"fill-muted stroke-border-strong\" />\n            </g>\n          </motion.g>\n          <motion.g {...float(reduce, 2.5, 3.8, 0.9)}>\n            <g transform=\"rotate(8 74 48)\">\n              <rect x=\"52\" y=\"20\" width=\"44\" height=\"56\" rx=\"7\" strokeWidth=\"1.5\" className=\"fill-muted stroke-border-strong\" />\n            </g>\n          </motion.g>\n          <motion.g {...float(reduce, 3.5, 3, 0)}>\n            <rect x=\"37\" y=\"16\" width=\"46\" height=\"60\" rx=\"7\" strokeWidth=\"1.5\" className=\"fill-card stroke-border-strong\" />\n            <motion.path d=\"M46 30h20\" strokeWidth=\"3\" strokeLinecap=\"round\" className=\"stroke-foreground/70\" {...draw(0.25, reduce)} />\n            <motion.path d=\"M46 40h28\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.35, reduce)} />\n            <motion.path d=\"M46 49h24\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.45, reduce)} />\n            <motion.path d=\"M46 58h16\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.55, reduce)} />\n            <motion.g\n              initial={{ opacity: 0, y: reduce ? 0 : 4 }}\n              whileInView={{ opacity: 1, y: 0 }}\n              viewport={{ once: true }}\n              transition={{ duration: 0.3, ease: EASE_OUT, delay: 0.6 }}\n            >\n              <circle cx=\"84\" cy=\"17\" r=\"9\" className=\"fill-primary\" />\n              <path d=\"M84 13v8M80 17h8\" strokeWidth=\"2\" strokeLinecap=\"round\" className=\"stroke-primary-foreground\" />\n            </motion.g>\n          </motion.g>\n        </>\n      ) : (\n        <>\n          <motion.g {...float(reduce, 1.5, 3.6, 0.5)}>\n            <rect x=\"22\" y=\"22\" width=\"64\" height=\"50\" rx=\"8\" strokeWidth=\"1.5\" className=\"fill-muted stroke-border-strong\" />\n            <motion.path d=\"M32 36h30\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.2, reduce)} />\n            <motion.path d=\"M32 46h22\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.3, reduce)} />\n            <motion.path d=\"M32 56h26\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.4, reduce)} />\n          </motion.g>\n          <motion.g {...float(reduce, 3.5, 3, 0)}>\n            <circle cx=\"76\" cy=\"48\" r=\"17\" strokeWidth=\"1.5\" className=\"fill-card stroke-border-strong\" />\n            <motion.circle cx=\"76\" cy=\"48\" r=\"11\" strokeWidth=\"3\" className=\"stroke-foreground/70\" {...draw(0.35, reduce)} />\n            <motion.path d=\"M84.5 56.5 96 68\" strokeWidth=\"5\" strokeLinecap=\"round\" className=\"stroke-foreground/70\" {...draw(0.6, reduce)} />\n            <motion.path d=\"M72.5 44.5l7 7M79.5 44.5l-7 7\" strokeWidth=\"2\" strokeLinecap=\"round\" className=\"stroke-muted-foreground\" {...draw(0.75, reduce)} />\n          </motion.g>\n        </>\n      )}\n    </svg>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                 EmptyState                                 */\n/* -------------------------------------------------------------------------- */\n\nexport interface EmptyStateProps {\n  title: ReactNode;\n  description?: ReactNode;\n  /**\n   * Icon or illustration slot. Defaults to `<EmptyStateIllustration />`;\n   * pass `null` for none.\n   */\n  icon?: ReactNode;\n  /** Primary call to action (e.g. a library `Button`). */\n  action?: ReactNode;\n  /** Secondary call to action, rendered after the primary. */\n  secondaryAction?: ReactNode;\n  size?: EmptyStateSize;\n  /** Dashed border container, for empty regions inside a page. */\n  bordered?: boolean;\n  /** Heading element for the title. Default `h3`. */\n  titleAs?: \"h2\" | \"h3\" | \"h4\" | \"p\";\n  /** Extra content under the actions (links, hints). */\n  children?: ReactNode;\n  className?: string;\n}\n\nconst SIZE: Record<\n  EmptyStateSize,\n  { root: string; title: string; description: string; art: number }\n> = {\n  sm: { root: \"gap-3 px-4 py-6\", title: \"text-sm\", description: \"text-xs max-w-[32ch]\", art: 88 },\n  md: { root: \"gap-4 px-6 py-10\", title: \"text-base\", description: \"text-sm max-w-[38ch]\", art: 120 },\n  lg: { root: \"gap-5 px-8 py-14\", title: \"text-lg\", description: \"text-base max-w-[44ch]\", art: 152 },\n};\n\n/**\n * Empty-state block: illustration, title, description and actions that\n * stagger in when scrolled into view (opacity + small rise; opacity only under\n * reduced motion).\n */\nexport function EmptyState({\n  title,\n  description,\n  icon,\n  action,\n  secondaryAction,\n  size = \"md\",\n  bordered = false,\n  titleAs: Title = \"h3\",\n  children,\n  className,\n}: EmptyStateProps) {\n  const reduce = useReducedMotion();\n  const s = SIZE[size];\n\n  const container: Variants = {\n    hidden: {},\n    show: { transition: { staggerChildren: 0.07, delayChildren: 0.05 } },\n  };\n  const item: Variants = {\n    hidden: { opacity: 0, y: reduce ? 0 : 8 },\n    show: { opacity: 1, y: 0, transition: { duration: 0.32, ease: EASE_OUT } },\n  };\n\n  const art = icon === undefined ? <EmptyStateIllustration size={s.art} /> : icon;\n\n  return (\n    <motion.div\n      variants={container}\n      initial=\"hidden\"\n      whileInView=\"show\"\n      viewport={{ once: true, amount: 0.4 }}\n      className={cn(\n        \"flex w-full flex-col items-center text-center\",\n        s.root,\n        bordered && \"rounded-2xl border border-dashed border-border-strong\",\n        className,\n      )}\n    >\n      {art != null ? (\n        <motion.div variants={item} className=\"flex items-center justify-center text-muted-foreground\">\n          {art}\n        </motion.div>\n      ) : null}\n      <div className=\"flex flex-col items-center gap-1.5\">\n        <motion.div variants={item}>\n          <Title className={cn(\"font-semibold tracking-tight text-foreground\", s.title)}>\n            {title}\n          </Title>\n        </motion.div>\n        {description != null ? (\n          <motion.p variants={item} className={cn(\"text-muted-foreground text-balance\", s.description)}>\n            {description}\n          </motion.p>\n        ) : null}\n      </div>\n      {action != null || secondaryAction != null ? (\n        <motion.div variants={item} className=\"flex flex-wrap items-center justify-center gap-2\">\n          {action}\n          {secondaryAction}\n        </motion.div>\n      ) : null}\n      {children != null ? <motion.div variants={item}>{children}</motion.div> : null}\n    </motion.div>\n  );\n}\n"},{"path":"components/motion/kbd.tsx","type":"registry:component","target":"@components/motion/kbd.tsx","content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { Fragment, type ReactNode, useEffect, useState } from \"react\";\nimport { SPRING_PRESS } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\n/* -------------------------------------------------------------------------- */\n/*                                  Platform                                  */\n/* -------------------------------------------------------------------------- */\n\nfunction detectApple() {\n  const nav = navigator as Navigator & {\n    userAgentData?: { platform?: string };\n  };\n  const platform = nav.userAgentData?.platform ?? nav.platform ?? \"\";\n  return /mac|iphone|ipad|ipod/i.test(platform);\n}\n\n/**\n * True on Apple platforms. Always `true` on the server and the first client\n * render (so markup matches), then corrected after mount — `Mod` therefore\n * renders ⌘ first and swaps to Ctrl on other platforms.\n */\nexport function useIsApple() {\n  const [apple, setApple] = useState(true);\n  useEffect(() => setApple(detectApple()), []);\n  return apple;\n}\n\ntype KeyLabel = { glyph: string; name: string };\n\n/** Display glyph + spoken name for a key token like \"mod\", \"shift\", \"k\". */\nexport function keyLabel(key: string, apple: boolean): KeyLabel {\n  const k = key.toLowerCase();\n  switch (k) {\n    case \"mod\":\n      return apple\n        ? { glyph: \"⌘\", name: \"Command\" }\n        : { glyph: \"Ctrl\", name: \"Control\" };\n    case \"cmd\":\n    case \"command\":\n    case \"meta\":\n      return apple\n        ? { glyph: \"⌘\", name: \"Command\" }\n        : { glyph: \"Win\", name: \"Windows\" };\n    case \"ctrl\":\n    case \"control\":\n      return apple\n        ? { glyph: \"⌃\", name: \"Control\" }\n        : { glyph: \"Ctrl\", name: \"Control\" };\n    case \"shift\":\n      return apple\n        ? { glyph: \"⇧\", name: \"Shift\" }\n        : { glyph: \"Shift\", name: \"Shift\" };\n    case \"alt\":\n    case \"option\":\n      return apple\n        ? { glyph: \"⌥\", name: \"Option\" }\n        : { glyph: \"Alt\", name: \"Alt\" };\n    case \"enter\":\n    case \"return\":\n      return { glyph: \"↵\", name: \"Enter\" };\n    case \"esc\":\n    case \"escape\":\n      return { glyph: \"Esc\", name: \"Escape\" };\n    case \"backspace\":\n      return { glyph: \"⌫\", name: \"Backspace\" };\n    case \"delete\":\n      return { glyph: \"⌦\", name: \"Delete\" };\n    case \"tab\":\n      return { glyph: \"⇥\", name: \"Tab\" };\n    case \"space\":\n      return { glyph: \"Space\", name: \"Space\" };\n    case \"up\":\n    case \"arrowup\":\n      return { glyph: \"↑\", name: \"Up arrow\" };\n    case \"down\":\n    case \"arrowdown\":\n      return { glyph: \"↓\", name: \"Down arrow\" };\n    case \"left\":\n    case \"arrowleft\":\n      return { glyph: \"←\", name: \"Left arrow\" };\n    case \"right\":\n    case \"arrowright\":\n      return { glyph: \"→\", name: \"Right arrow\" };\n    default:\n      return key.length === 1\n        ? { glyph: key.toUpperCase(), name: key.toUpperCase() }\n        : { glyph: key, name: key };\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                               Key listening                                */\n/* -------------------------------------------------------------------------- */\n\n/** Does this keyboard event's key correspond to the token? */\nfunction eventMatches(token: string, event: KeyboardEvent, apple: boolean) {\n  const k = token.toLowerCase();\n  const key = event.key.toLowerCase();\n  switch (k) {\n    case \"mod\":\n      return key === (apple ? \"meta\" : \"control\");\n    case \"cmd\":\n    case \"command\":\n    case \"meta\":\n      return key === \"meta\";\n    case \"ctrl\":\n    case \"control\":\n      return key === \"control\";\n    case \"alt\":\n    case \"option\":\n      return key === \"alt\";\n    case \"esc\":\n      return key === \"escape\";\n    case \"return\":\n      return key === \"enter\";\n    case \"space\":\n      return event.code === \"Space\";\n    case \"up\":\n    case \"down\":\n    case \"left\":\n    case \"right\":\n      return key === `arrow${k}`;\n    default:\n      // Letters and digits by physical key, so Shift/Option variants\n      // (\"P\" with Shift, \"π\" with Option) still match.\n      if (/^[a-z]$/.test(k)) return event.code === `Key${k.toUpperCase()}`;\n      if (/^[0-9]$/.test(k)) return event.code === `Digit${k}`;\n      return key === k;\n  }\n}\n\nconst MODIFIERS = new Set([\"meta\", \"control\", \"shift\", \"alt\"]);\n\n/**\n * Tracks which of `keys` are physically held. Listens on window without\n * preventing default, so it never steals the shortcut from the page.\n */\nexport function useKeysPressed(keys: string[], enabled = true) {\n  const apple = useIsApple();\n  const [held, setHeld] = useState<boolean[]>(() => keys.map(() => false));\n  const signature = keys.join(\"+\");\n\n  useEffect(() => {\n    const tokens = signature.split(\"+\");\n    setHeld(tokens.map(() => false));\n    if (!enabled) return;\n    const down = new Set<number>();\n    const sync = () => setHeld(tokens.map((_, i) => down.has(i)));\n\n    const onDown = (event: KeyboardEvent) => {\n      let changed = false;\n      tokens.forEach((token, i) => {\n        if (!down.has(i) && eventMatches(token, event, apple)) {\n          down.add(i);\n          changed = true;\n        }\n      });\n      if (changed) sync();\n    };\n    const onUp = (event: KeyboardEvent) => {\n      // macOS swallows keyup for other keys while ⌘ is held, so releasing a\n      // modifier releases everything.\n      if (MODIFIERS.has(event.key.toLowerCase()) && apple) {\n        down.clear();\n        sync();\n        return;\n      }\n      let changed = false;\n      tokens.forEach((token, i) => {\n        if (down.has(i) && eventMatches(token, event, apple)) {\n          down.delete(i);\n          changed = true;\n        }\n      });\n      if (changed) sync();\n    };\n    const reset = () => {\n      down.clear();\n      sync();\n    };\n    window.addEventListener(\"keydown\", onDown);\n    window.addEventListener(\"keyup\", onUp);\n    window.addEventListener(\"blur\", reset);\n    return () => {\n      window.removeEventListener(\"keydown\", onDown);\n      window.removeEventListener(\"keyup\", onUp);\n      window.removeEventListener(\"blur\", reset);\n    };\n  }, [signature, enabled, apple]);\n\n  return held;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Kbd                                     */\n/* -------------------------------------------------------------------------- */\n\ntype KbdSize = \"sm\" | \"md\";\n\nconst SIZE: Record<KbdSize, string> = {\n  sm: \"h-5 min-w-5 px-1 text-[11px] rounded-[5px] border-b-[2px]\",\n  md: \"h-6 min-w-6 px-1.5 text-xs rounded-md border-b-[3px]\",\n};\n\n// How far the key drops when pressed, in px — matches the thicker bottom edge\n// so the cap visibly sinks into its base.\nconst DIP: Record<KbdSize, number> = { sm: 1.5, md: 2 };\n\nexport interface KbdProps {\n  /** A key token (\"mod\", \"shift\", \"k\", \"enter\"…) or any custom content. */\n  children: ReactNode;\n  size?: KbdSize;\n  /** Show the key pressed (spring dip). */\n  pressed?: boolean;\n  /** Dip when the real key is held. Needs `children` to be a key token string. */\n  listen?: boolean;\n  className?: string;\n}\n\n/**\n * A single keyboard key. String children are treated as key tokens, so\n * `<Kbd>mod</Kbd>` renders ⌘ on Apple platforms and Ctrl elsewhere.\n */\nexport function Kbd({\n  children,\n  size = \"md\",\n  pressed = false,\n  listen = false,\n  className,\n}: KbdProps) {\n  const apple = useIsApple();\n  const token = typeof children === \"string\" ? children : null;\n  const [held] = useKeysPressed(token ? [token] : [], listen && token !== null);\n  const label = token ? keyLabel(token, apple) : null;\n\n  return (\n    <KeyCap\n      size={size}\n      pressed={pressed || Boolean(held)}\n      title={label?.name}\n      className={className}\n    >\n      {label ? label.glyph : children}\n    </KeyCap>\n  );\n}\n\nfunction KeyCap({\n  children,\n  size,\n  pressed,\n  title,\n  className,\n}: {\n  children: ReactNode;\n  size: KbdSize;\n  pressed: boolean;\n  title?: string;\n  className?: string;\n}) {\n  const reduce = useReducedMotion() ?? false;\n  return (\n    <motion.kbd\n      title={title}\n      data-pressed={pressed ? \"\" : undefined}\n      initial={false}\n      animate={\n        reduce\n          ? { opacity: pressed ? 0.7 : 1 }\n          : { y: pressed ? DIP[size] : 0, scale: pressed ? 0.96 : 1 }\n      }\n      transition={reduce ? { duration: 0.1 } : SPRING_PRESS}\n      className={cn(\n        \"inline-flex select-none items-center justify-center border border-border border-b-border-strong bg-muted font-sans font-medium leading-none text-muted-foreground\",\n        \"data-[pressed]:border-b-border data-[pressed]:bg-foreground/[0.08] data-[pressed]:text-foreground\",\n        SIZE[size],\n        className,\n      )}\n    >\n      {children}\n    </motion.kbd>\n  );\n}\n\nexport interface KbdComboProps {\n  /** Key tokens, e.g. [\"mod\", \"shift\", \"p\"]. */\n  keys: string[];\n  size?: KbdSize;\n  /** Show every key pressed. */\n  pressed?: boolean;\n  /** Dip each key while the real key is held. */\n  listen?: boolean;\n  /** Text between keys. Default none on Apple, \"+\" elsewhere. */\n  separator?: ReactNode;\n  className?: string;\n}\n\n/**\n * A key combination: one chip per key, platform-aware, with a spoken label\n * (\"Command + Shift + P\") for assistive tech.\n */\nexport function KbdCombo({\n  keys,\n  size = \"md\",\n  pressed = false,\n  listen = false,\n  separator,\n  className,\n}: KbdComboProps) {\n  const apple = useIsApple();\n  const held = useKeysPressed(keys, listen);\n  const labels = keys.map((key) => keyLabel(key, apple));\n  const sep = separator === undefined ? (apple ? null : \"+\") : separator;\n\n  return (\n    <kbd\n      className={cn(\n        \"inline-flex items-center gap-1 font-sans text-muted-foreground\",\n        className,\n      )}\n    >\n      <span className=\"sr-only\">{labels.map((l) => l.name).join(\" + \")}</span>\n      <span aria-hidden=\"true\" className=\"inline-flex items-center gap-1\">\n        {labels.map((label, i) => (\n          <Fragment key={keys[i]}>\n            {i > 0 && sep !== null ? (\n              <span className=\"text-[10px] text-muted-foreground/70\">{sep}</span>\n            ) : null}\n            <KeyCap size={size} pressed={pressed || Boolean(held[i])}>\n              {label.glyph}\n            </KeyCap>\n          </Fragment>\n        ))}\n      </span>\n    </kbd>\n  );\n}\n"},{"path":"components/motion/number-ticker.tsx","type":"registry:component","target":"@components/motion/number-ticker.tsx","content":"\"use client\";\n\nimport { animate, motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface NumberTickerProps {\n  value: number;\n  /** Digits to pad to (left). */\n  pad?: number;\n  /** Per-digit roll duration in seconds. */\n  duration?: number;\n  /** Stagger between digits. */\n  stagger?: number;\n  /** Render only after the element enters the viewport. */\n  startOnView?: boolean;\n  prefix?: string;\n  suffix?: string;\n  /** Add a small blur during digit rolls. */\n  blur?: boolean;\n  className?: string;\n  digitClassName?: string;\n  /** Insert en-US group separators (commas); fixed so server and client match. Server-component safe. */\n  locale?: boolean;\n  /** Custom formatter. Client-only — server components must use `locale` instead. */\n  format?: (value: number) => string;\n}\n\nconst DIGIT_HEIGHT_EM = 1.1;\nconst DIGITS = Array.from({ length: 10 }, (_, n) => n);\n\nexport function NumberTicker({\n  value,\n  pad,\n  duration = 0.9,\n  stagger = 0.04,\n  startOnView = true,\n  prefix,\n  suffix,\n  blur = false,\n  className,\n  digitClassName,\n  locale,\n  format,\n}: NumberTickerProps) {\n  const containerRef = useRef<HTMLSpanElement>(null);\n  const inView = useInView(containerRef, { once: true, amount: 0.6 });\n  const [armed, setArmed] = useState(!startOnView);\n\n  useEffect(() => {\n    if (startOnView && inView) setArmed(true);\n  }, [startOnView, inView]);\n\n  const text = useMemo(() => {\n    const rounded = Math.round(value);\n    const formatted = format\n      ? format(rounded)\n      : locale\n        ? rounded.toLocaleString(\"en-US\")\n        : rounded.toString();\n    return pad ? formatted.padStart(pad, \"0\") : formatted;\n  }, [value, pad, format, locale]);\n  const glyphs = useMemo(() => {\n    const chars = text.split(\"\");\n    // Key by place value (position from the right): a changing digit keeps its\n    // identity and rolls to the new value instead of remounting and replaying\n    // from 0. Growing numbers add glyphs on the left without re-keying the\n    // ones, tens, hundreds already on screen.\n    return chars.map((char, i) => ({ char, id: `g-${chars.length - 1 - i}` }));\n  }, [text]);\n  const readableText = `${prefix ?? \"\"}${text}${suffix ?? \"\"}`;\n\n  // Stagger is an entrance flourish. Once the reveal has played, value\n  // changes roll every digit immediately — a per-digit delay on live updates\n  // reads as lag.\n  const [entered, setEntered] = useState(false);\n  useEffect(() => {\n    if (!armed || entered) return;\n    const total = (duration + glyphs.length * stagger) * 1000;\n    const t = window.setTimeout(() => setEntered(true), total);\n    return () => window.clearTimeout(t);\n  }, [armed, entered, duration, stagger, glyphs.length]);\n\n  return (\n    <span\n      ref={containerRef}\n      className={cn(\"inline-flex items-center tabular-nums\", className)}\n    >\n      <span className=\"sr-only\">{readableText}</span>\n      <span aria-hidden=\"true\" className=\"inline-flex items-center\">\n        {prefix ? <span>{prefix}</span> : null}\n        {glyphs.map(({ char, id }, i) => {\n          const isDigit = /\\d/.test(char);\n          if (!isDigit) {\n            return (\n              <span key={id} className=\"inline-block\">\n                {char}\n              </span>\n            );\n          }\n          const digit = Number(char);\n          return (\n            <Digit\n              key={id}\n              digit={armed ? digit : 0}\n              delay={entered ? 0 : i * stagger}\n              duration={duration}\n              blur={blur}\n              className={digitClassName}\n            />\n          );\n        })}\n        {suffix ? <span>{suffix}</span> : null}\n      </span>\n    </span>\n  );\n}\n\nfunction Digit({\n  digit,\n  delay,\n  duration,\n  blur,\n  className,\n}: {\n  digit: number;\n  delay: number;\n  duration: number;\n  blur: boolean;\n  className?: string;\n}) {\n  const reduce = useReducedMotion();\n  const columnRef = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    if (reduce || !blur || !columnRef.current || !Number.isFinite(digit)) {\n      return;\n    }\n\n    const node = columnRef.current;\n    const controls = animate(\n      node,\n      { filter: [\"blur(10px)\", \"blur(0px)\"] },\n      {\n        duration: Math.min(duration * 0.75, 0.32),\n        delay,\n        ease: EASE_OUT,\n      },\n    );\n\n    return () => {\n      controls.stop();\n      node.style.filter = \"blur(0px)\";\n    };\n  }, [blur, delay, digit, duration, reduce]);\n\n  return (\n    <span\n      className={cn(\"relative inline-block overflow-hidden\", className)}\n      style={{ height: `${DIGIT_HEIGHT_EM}em`, width: \"1ch\" }}\n    >\n      <motion.span\n        ref={columnRef}\n        initial={{ y: 0 }}\n        animate={{ y: `-${digit * DIGIT_HEIGHT_EM}em` }}\n        transition={\n          reduce\n            ? { duration: 0 }\n            : { duration, delay, ease: EASE_OUT }\n        }\n        className=\"absolute inset-x-0 top-0 flex flex-col items-center will-change-[transform,filter]\"\n      >\n        {DIGITS.map((n) => (\n          <span\n            key={n}\n            className=\"flex h-[1.1em] items-center justify-center leading-none\"\n          >\n            {n}\n          </span>\n        ))}\n      </motion.span>\n    </span>\n  );\n}\n"},{"path":"components/motion/tabs.tsx","type":"registry:component","target":"@components/motion/tabs.tsx","content":"\"use client\";\n\nimport { motion, MotionConfig, useReducedMotion, type Transition } from \"motion/react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { TOUCH_TARGET_CLASS } from \"@/lib/touch\";\nimport { cn } from \"@/lib/utils\";\n\ntype Variant = \"pill\" | \"underline\" | \"segment\";\n\ntype Ctx = {\n  value: string;\n  setValue: (v: string) => void;\n  layoutId: string;\n  variant: Variant;\n};\n\nconst TabsCtx = createContext<Ctx | null>(null);\n\nfunction useTabs() {\n  const ctx = useContext(TabsCtx);\n  if (!ctx) throw new Error(\"Tabs.* must be used inside <Tabs>\");\n  return ctx;\n}\n\n// Weighty spring for the active-tab indicator: a touch of overshoot so it\n// settles with life instead of snapping.\nconst transition: Transition = {\n  type: \"spring\",\n  stiffness: 170,\n  damping: 24,\n  mass: 1.2,\n};\n\nexport function Tabs({\n  defaultValue,\n  value,\n  onValueChange,\n  variant = \"pill\",\n  children,\n  className,\n}: {\n  defaultValue?: string;\n  value?: string;\n  onValueChange?: (v: string) => void;\n  variant?: Variant;\n  children: ReactNode;\n  className?: string;\n}) {\n  const [internal, setInternal] = useState(defaultValue ?? \"\");\n  const layoutId = useId();\n  const reduce = useReducedMotion();\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n  const setValue = useCallback(\n    (v: string) => {\n      if (!controlled) setInternal(v);\n      onValueChange?.(v);\n    },\n    [controlled, onValueChange],\n  );\n  const contextValue = useMemo(\n    () => ({ value: current, setValue, layoutId, variant }),\n    [current, layoutId, setValue, variant],\n  );\n  return (\n    <MotionConfig transition={reduce ? { duration: 0 } : transition}>\n      <TabsCtx.Provider value={contextValue}>\n        {/* layoutRoot: the indicator's layoutId measures in page coordinates, so\n            inside fixed/scrolled containers it would replay scroll offsets as\n            movement. The pill only ever travels within the list, so scoping\n            projection to the Tabs wrapper is always correct. */}\n        <motion.div layoutRoot className={className}>\n          {children}\n        </motion.div>\n      </TabsCtx.Provider>\n    </MotionConfig>\n  );\n}\n\nconst listClasses: Record<Variant, string> = {\n  pill: \"inline-flex items-center gap-1 rounded-full bg-card p-1\",\n  underline: \"inline-flex items-center gap-1 border-b border-border\",\n  segment: \"inline-flex items-center gap-0 rounded-lg bg-card p-0.5\",\n};\n\nexport function TabsList({ children, className }: { children: ReactNode; className?: string }) {\n  const { variant } = useTabs();\n  return (\n    <div role=\"tablist\" className={cn(listClasses[variant], className)}>\n      {children}\n    </div>\n  );\n}\n\nexport function TabsTrigger({\n  value,\n  children,\n  className,\n  indicatorClassName,\n}: {\n  value: string;\n  children: ReactNode;\n  className?: string;\n  indicatorClassName?: string;\n}) {\n  const { value: current, setValue, layoutId, variant } = useTabs();\n  const active = current === value;\n\n  if (variant === \"underline\") {\n    return (\n      <button\n        type=\"button\"\n        role=\"tab\"\n        aria-selected={active}\n        onClick={() => setValue(value)}\n        className={cn(\n          \"relative isolate px-3 pb-2.5 pt-1 -mb-px text-sm font-medium transition-colors min-h-[44px] inline-flex items-center\",\n          active ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n          className,\n        )}\n      >\n        {children}\n        {active ? (\n        <motion.span\n          layoutId={layoutId}\n          layout=\"position\"\n          className={cn(\n            \"absolute -bottom-px left-0 right-0 h-px bg-primary\",\n            indicatorClassName,\n          )}\n        />\n        ) : null}\n      </button>\n    );\n  }\n\n  const radius = variant === \"pill\" ? \"rounded-full\" : \"rounded-md\";\n\n  return (\n    <div className=\"relative\">\n      {active ? (\n        <motion.span\n          layoutId={layoutId}\n          layout=\"position\"\n          style={{ borderRadius: variant === \"pill\" ? 9999 : 8 }}\n          className={cn(\n            \"absolute inset-0 bg-primary\",\n            radius,\n            indicatorClassName,\n          )}\n        />\n      ) : null}\n      <button\n        type=\"button\"\n        role=\"tab\"\n        aria-selected={active}\n        onClick={() => setValue(value)}\n        className={cn(\n          \"relative z-10 inline-flex items-center justify-center whitespace-nowrap bg-transparent px-3.5 py-1.5 text-sm font-medium outline-none\",\n          TOUCH_TARGET_CLASS,\n          \"transition-colors\",\n          active\n            ? \"text-primary-foreground\"\n            : \"text-muted-foreground hover:text-foreground\",\n          radius,\n          className,\n        )}\n      >\n        {children}\n      </button>\n    </div>\n  );\n}\n\nexport function TabsContent({ value, children, className }: { value: string; children: ReactNode; className?: string }) {\n  const { value: current } = useTabs();\n  const reduce = useReducedMotion();\n  const active = current === value;\n  // Inactive panels stay mounted but hidden, so their content (e.g. source\n  // code) is present in the server-rendered HTML for crawlers and assistive\n  // tech, instead of being dropped from the DOM.\n  if (!active) {\n    return (\n      <div hidden className={className}>\n        {children}\n      </div>\n    );\n  }\n  return (\n    <motion.div\n      key={value}\n      initial={{ opacity: 0, y: reduce ? 0 : 4 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.18, ease: EASE_OUT }}\n      className={cn(\"mt-4\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"components/motion/button/base.tsx","type":"registry:component","target":"@components/motion/button/base.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type PointerEvent,\n  type ReactNode,\n  useCallback,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { TOUCH_TARGET_CLASS } from \"@/lib/touch\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"outline\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\nexport interface ButtonProps extends Omit<\n  HTMLMotionProps<\"button\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  /** Spawn a Material-style ripple from the press point. Off by default. */\n  ripple?: boolean;\n  children?: ReactNode;\n}\n\nexport interface ButtonLinkProps extends Omit<\n  HTMLMotionProps<\"a\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  children?: ReactNode;\n}\n\ntype Ripple = { id: number; x: number; y: number; size: number };\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n  secondary: \"border border-border bg-card text-foreground hover:border-border\",\n  ghost: \"text-muted-foreground hover:text-foreground hover:bg-primary/5\",\n  outline:\n    \"border border-border bg-transparent text-foreground hover:bg-primary/5\",\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: \"h-8 px-3 text-xs gap-1.5 rounded-full\",\n  md: \"h-10 px-5 text-sm gap-2 rounded-full\",\n  lg: \"h-12 px-6 text-base gap-2 rounded-full\",\n  icon: \"h-8 w-8 rounded-lg\",\n};\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  function Button(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      ripple = false,\n      className,\n      children,\n      onPointerDown,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n    const [ripples, setRipples] = useState<Ripple[]>([]);\n    const nextId = useRef(0);\n\n    const handlePointerDown = useCallback(\n      (event: PointerEvent<HTMLButtonElement>) => {\n        if (ripple && !reduce) {\n          const rect = event.currentTarget.getBoundingClientRect();\n          const size = Math.max(rect.width, rect.height) * 2;\n          const id = nextId.current++;\n          setRipples((prev) => [\n            ...prev,\n            {\n              id,\n              x: event.clientX - rect.left,\n              y: event.clientY - rect.top,\n              size,\n            },\n          ]);\n        }\n        onPointerDown?.(event);\n      },\n      [ripple, reduce, onPointerDown],\n    );\n\n    return (\n      <motion.button\n        ref={ref}\n        type=\"button\"\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        onPointerDown={handlePointerDown}\n        className={cn(\n          \"relative inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          // The ripple layer clips itself, so on a touch screen the button can\n          // stop clipping and let its enlarged hit area overhang.\n          ripple && \"overflow-hidden pointer-coarse:overflow-visible\",\n          TOUCH_TARGET_CLASS,\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {ripple && !reduce ? (\n          <span className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]\">\n            <AnimatePresence>\n              {ripples.map((r) => (\n                <motion.span\n                  key={r.id}\n                  className=\"absolute rounded-full bg-current\"\n                  style={{\n                    left: r.x,\n                    top: r.y,\n                    width: r.size,\n                    height: r.size,\n                    x: \"-50%\",\n                    y: \"-50%\",\n                  }}\n                  initial={{ scale: 0.05, opacity: 0.3 }}\n                  animate={{ scale: 1, opacity: 0 }}\n                  exit={{ opacity: 0 }}\n                  transition={{ duration: 1.6, ease: EASE_OUT }}\n                  onAnimationComplete={() =>\n                    setRipples((prev) => prev.filter((x) => x.id !== r.id))\n                  }\n                />\n              ))}\n            </AnimatePresence>\n          </span>\n        ) : null}\n        {children}\n      </motion.button>\n    );\n  },\n);\n\nexport const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(\n  function ButtonLink(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      className,\n      children,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n\n    return (\n      <motion.a\n        ref={ref}\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        className={cn(\n          \"relative inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          TOUCH_TARGET_CLASS,\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {children}\n      </motion.a>\n    );\n  },\n);\n"},{"path":"components/motion/button/magnetic.tsx","type":"registry:component","target":"@components/motion/button/magnetic.tsx","content":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Magnetic } from \"../magnetic\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport interface MagneticButtonProps extends ButtonProps {\n  /** Magnetic pull strength. Default 0.25. */\n  strength?: number;\n  /** Class applied to the magnetic wrapper. */\n  magneticClassName?: string;\n}\n\nexport const MagneticButton = forwardRef<HTMLButtonElement, MagneticButtonProps>(function MagneticButton(\n  { strength = 0.25, magneticClassName, children, ...rest },\n  ref,\n) {\n  return (\n    <Magnetic strength={strength} className={magneticClassName}>\n      <Button ref={ref} {...rest}>\n        {children}\n      </Button>\n    </Magnetic>\n  );\n});\n"},{"path":"components/motion/button/stateful.tsx","type":"registry:component","target":"@components/motion/button/stateful.tsx","content":"\"use client\";\n\nimport { Check, Loader2, X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type ReactNode,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport type ButtonState = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nexport interface StatefulButtonProps extends Omit<ButtonProps, \"children\"> {\n  state?: ButtonState;\n  children: ReactNode;\n  loadingText?: ReactNode;\n  successText?: ReactNode;\n  errorText?: ReactNode;\n  icon?: ReactNode;\n}\n\nconst CASCADE_STAGGER = 0.025;\nconst ROLL_BLUR = \"blur(6px)\";\n\nconst CASCADE_LETTER_VARIANTS: Variants = {\n  initial: { opacity: 0, y: \"105%\", filter: ROLL_BLUR },\n  animate: (delay: number = 0) => ({\n    opacity: 1,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: { ...SPRING_SWAP, delay },\n  }),\n  exit: (delay: number = 0) => ({\n    opacity: 0,\n    y: \"-105%\",\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT, delay: delay * 0.5 },\n  }),\n};\n\nconst ICON_VARIANTS: Variants = {\n  // Width collapses too, so the icon adds/removes its own space smoothly\n  // instead of popping the row width in a single frame.\n  initial: { opacity: 0, width: 0, scale: 0.7, filter: ROLL_BLUR },\n  animate: {\n    opacity: 1,\n    width: \"1.5rem\",\n    scale: 1,\n    filter: \"blur(0px)\",\n    transition: SPRING_SWAP,\n  },\n  exit: {\n    opacity: 0,\n    width: 0,\n    scale: 0.7,\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT },\n  },\n};\n\nfunction IconSlot({ keyId, children }: { keyId: string; children: ReactNode }) {\n  const reduce = useReducedMotion();\n  return (\n    <motion.span\n      key={keyId}\n      variants={ICON_VARIANTS}\n      initial={reduce ? { opacity: 0 } : \"initial\"}\n      animate={reduce ? { opacity: 1 } : \"animate\"}\n      exit={reduce ? { opacity: 0 } : \"exit\"}\n      transition={reduce ? { duration: 0.15 } : undefined}\n      className=\"inline-grid shrink-0 place-items-center overflow-hidden\"\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nfunction TextSlot({\n  value,\n  children,\n}: {\n  value: string;\n  children: ReactNode;\n}) {\n  const reduce = useReducedMotion();\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const [width, setWidth] = useState<number>();\n  const label = typeof children === \"string\" ? children : null;\n  const cascade = label !== null && !reduce;\n\n  // Measure strings with the same per-letter layout as the cascade. Measuring\n  // the whole string preserves kerning, which can make it narrower than the\n  // inline-block letters and clip the final glyph during the width animation.\n  useLayoutEffect(() => {\n    const nextWidth = measureRef.current?.offsetWidth;\n    if (!nextWidth) return;\n    setWidth((current) => (current === nextWidth ? current : nextWidth));\n  });\n\n  return (\n    <motion.span\n      initial={false}\n      animate={{ width }}\n      transition={reduce ? { duration: 0 } : SPRING_SWAP}\n      className=\"relative inline-block overflow-hidden whitespace-nowrap align-bottom\"\n    >\n      <span\n        ref={measureRef}\n        aria-hidden\n        className=\"invisible inline-block whitespace-nowrap\"\n      >\n        {cascade\n          ? label.split(\"\").map((char, index) => (\n              <span\n                // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                key={index}\n                className=\"inline-block whitespace-pre\"\n              >\n                {char}\n              </span>\n            ))\n          : children}\n      </span>\n\n      {cascade ? (\n        <>\n          <span className=\"sr-only\">{label}</span>\n          <AnimatePresence initial={false}>\n            <motion.span\n              key={`cascade-${value}`}\n              aria-hidden\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              className=\"absolute left-0 top-0 inline-block whitespace-pre\"\n            >\n              {label.split(\"\").map((char, index) => (\n                <motion.span\n                  // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                  key={index}\n                  custom={index * CASCADE_STAGGER}\n                  variants={CASCADE_LETTER_VARIANTS}\n                  className=\"inline-block whitespace-pre will-change-[opacity,filter,transform]\"\n                >\n                  {char}\n                </motion.span>\n              ))}\n            </motion.span>\n          </AnimatePresence>\n        </>\n      ) : (\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={`text-${value}`}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 14, filter: ROLL_BLUR }}\n            animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -14, filter: ROLL_BLUR }}\n            transition={reduce ? { duration: 0.15 } : SPRING_SWAP}\n            className=\"absolute left-0 top-0 inline-block will-change-[opacity,filter,transform]\"\n          >\n            {children}\n          </motion.span>\n        </AnimatePresence>\n      )}\n    </motion.span>\n  );\n}\n\nexport const StatefulButton = forwardRef<HTMLButtonElement, StatefulButtonProps>(function StatefulButton(\n  {\n    state = \"idle\",\n    children,\n    loadingText = \"Loading\",\n    successText = \"Done\",\n    errorText = \"Try again\",\n    icon,\n    disabled,\n    ...rest\n  },\n  ref,\n) {\n  const isBusy = state === \"loading\";\n  const stateText =\n    state === \"loading\"\n      ? loadingText\n      : state === \"success\"\n        ? successText\n        : state === \"error\"\n        ? errorText\n        : children;\n  const textKey =\n    typeof stateText === \"string\" ? `${state}-${stateText}` : state;\n\n  return (\n    <Button ref={ref} disabled={disabled || isBusy} aria-busy={isBusy} whileHover={undefined} {...rest}>\n      <span\n        aria-live=\"polite\"\n        className=\"relative inline-flex items-center justify-center overflow-hidden\"\n      >\n        <AnimatePresence initial={false}>\n          {state === \"loading\" ? (\n            <IconSlot keyId=\"loading-icon\">\n              <Loader2 className=\"h-4 w-4 animate-spin\" />\n            </IconSlot>\n          ) : null}\n          {state === \"success\" ? (\n            <IconSlot keyId=\"success-icon\">\n              <Check className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n          {state === \"error\" ? (\n            <IconSlot keyId=\"error-icon\">\n              <X className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n        </AnimatePresence>\n\n        <TextSlot value={textKey}>{stateText}</TextSlot>\n\n        <AnimatePresence initial={false}>\n          {state === \"idle\" && icon ? (\n            <IconSlot keyId=\"idle-icon\">{icon}</IconSlot>\n          ) : null}\n        </AnimatePresence>\n      </span>\n    </Button>\n  );\n});\n"},{"path":"lib/touch.ts","type":"registry:lib","target":"@lib/touch.ts","content":"// Shared touch primitives. iOS and iPadOS run their own gestures on top of the\n// page — the long-press selection callout and the selection it drags in with\n// it — and they win: once the platform claims a touch it cancels ours\n// mid-gesture, so a press-and-hold or a drag simply dies. Surfaces that own\n// their gesture have to opt out.\n//\n// What the two classes below cover, precisely:\n// - `-webkit-touch-callout: none` stops iOS's long-press callout. WebKit-only:\n//   it is not a property other engines have, so it is inert everywhere else.\n// - `user-select: none` stops the long-press selection on every engine,\n//   Android included, and stops a drag from painting a selection under the\n//   cursor. It is inherited, so it reaches every descendant — which is why the\n//   two classes differ only in whether they apply it unconditionally.\n// What neither covers:\n// - Chrome for Android's long-press menu on a link or an image. No CSS\n//   suppresses it; a gesture surface that wraps one needs its own\n//   `onContextMenu` with `preventDefault()`.\n// - The native drag of an `<img>` or `<a>` descendant. `-webkit-user-drag` is\n//   not inherited and plain divs and buttons are not drag sources, so setting\n//   it on the surface does nothing — the child itself needs `draggable={false}`.\n\n/**\n * Classes for a surface that *is* the control: a thumb, a drum, a stage, a\n * handle, a hold button. Selection is suppressed on every input, because a\n * drag that highlights the control's own label is wrong on a mouse too.\n * Compose with `touch-none` when the surface also owns the scroll axis — leave\n * it off when the page must still scroll from there.\n */\nexport const TOUCH_GESTURE_CLASS = \"select-none [-webkit-touch-callout:none]\";\n\n/**\n * The same opt-out for a gesture surface that wraps content the consumer owns:\n * a scroller, a context-menu trigger, a sheet header, a list row. Selection is\n * suppressed only where the platform runs its own press gestures — a coarse\n * pointer — so a mouse user can still select and copy that content. If the\n * gesture itself would paint a selection under the cursor, add `select-none`\n * for the duration of the gesture rather than reaching for\n * `TOUCH_GESTURE_CLASS`.\n *\n * `pointer: coarse` describes the *primary* pointer and nothing else, so a\n * hybrid machine reads it wrong in both directions: a tablet with a mouse\n * plugged in keeps touch as primary and loses mouse selection, and a laptop\n * with a touchscreen keeps the mouse as primary and leaves selection live\n * under a finger. No media query can answer per interaction — the query is\n * about the device, and the question is about the gesture in progress. The\n * default stays here because it is right on the machines that are one thing or\n * the other, and losing a selection is a nuisance; where the miss costs a\n * *gesture* instead, the surface pairs it with `holdSelection` on the press.\n */\nexport const TOUCH_GESTURE_CONTENT_CLASS =\n  \"[-webkit-touch-callout:none] pointer-coarse:select-none\";\n\n/**\n * Grow a small control's *hit area* to at least 44×44px (WCAG 2.5.5, Apple\n * HIG) on a coarse pointer, without changing what it looks like or the space\n * it takes in layout. An invisible `::after` box, centered on the control and\n * `max(100%, 2.75rem)` on each axis, catches the taps: a 32px icon button\n * reaches 6px past each edge, a wide 40px button only grows vertically, and a\n * control already 44px or larger is left as it is. Hit-testing a pseudo\n * element lands on its owner, so handlers, focus and `:active` all behave.\n *\n * Mouse and trackpad rendering is untouched — nothing here applies outside\n * `pointer: coarse`. Requirements on the element it goes on:\n * - It must be the containing block: add `relative` (and keep it mergeable, so\n *   a consumer's `absolute` still wins through `cn`).\n * - It must not clip itself (`overflow-hidden`), or the overhang is clipped\n *   away along with its hit area.\n * - It must render children: `<input>` and other void elements have no\n *   `::after`. Put it on the wrapper or label instead.\n * - It must not already use `::after` for something visible.\n *\n * Centered 44px means two 32px buttons with a 4px gap overlap by 8px; the\n * later one wins that strip, which still ends well short of the earlier\n * button's visual center. Below a ~32px pitch the later neighbour starts\n * eating into that center — use `TOUCH_TARGET_Y_CLASS` for such dense rows.\n *\n * The overhang is real geometry: inside a scroll container it counts as\n * scrollable overflow, so a control flush against a scroller's edge can add a\n * few pixels of scroll on touch screens. Give such a scroller a little padding\n * on that side.\n */\nexport const TOUCH_TARGET_CLASS =\n  \"pointer-coarse:after:absolute pointer-coarse:after:top-1/2 pointer-coarse:after:left-1/2 pointer-coarse:after:size-[max(100%,2.75rem)] pointer-coarse:after:-translate-x-1/2 pointer-coarse:after:-translate-y-1/2 pointer-coarse:after:content-['']\";\n\n/**\n * `TOUCH_TARGET_CLASS` on the vertical axis only, for controls packed tighter\n * than ~32px apart in a row (pagination dots, dense swatches), where growing\n * sideways would hand most of each control to its neighbour. Same rules.\n */\nexport const TOUCH_TARGET_Y_CLASS =\n  \"pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:top-1/2 pointer-coarse:after:h-[max(100%,2.75rem)] pointer-coarse:after:-translate-y-1/2 pointer-coarse:after:content-['']\";\n\n/**\n * Suppress selection on `element` for as long as a gesture is running on it,\n * whatever the primary pointer of the machine happens to be. Returns the\n * release. Inline, so it wins over the class above and is gone again the\n * moment the gesture ends.\n *\n * For the press gestures a native selection would otherwise steal — a\n * long-press that opens a menu. Elsewhere prefer the classes: a surface that\n * takes selection away for the whole session is a surface whose text nobody\n * can copy.\n */\nexport function holdSelection(element: HTMLElement) {\n  element.style.setProperty(\"user-select\", \"none\");\n  element.style.setProperty(\"-webkit-user-select\", \"none\");\n  return () => {\n    element.style.removeProperty(\"user-select\");\n    element.style.removeProperty(\"-webkit-user-select\");\n  };\n}\n\n/**\n * Pointer capture, best effort. WebKit throws `NotFoundError` when the pointer\n * is already gone by the time the handler runs — routine on iOS, where the\n * system can claim the touch first — and an uncaught throw takes the rest of\n * the handler, the gesture included, down with it. Touch pointers carry\n * implicit capture anyway, so losing it is never fatal.\n */\nexport function capturePointer(element: Element, pointerId: number) {\n  try {\n    element.setPointerCapture(pointerId);\n  } catch {\n    // Pointer is no longer active — implicit capture still applies on touch.\n  }\n}\n\n/** Release a capture taken with `capturePointer`, ignoring a stale pointer. */\nexport function releasePointer(element: Element, pointerId: number) {\n  try {\n    if (element.hasPointerCapture(pointerId)) {\n      element.releasePointerCapture(pointerId);\n    }\n  } catch {\n    // Capture was already dropped by the browser.\n  }\n}\n\n/**\n * Whether this event came from a pointer that is *hovering*: not a touch, and\n * not currently pressed. Which input the user is holding right now is not\n * something a device capability can answer — a touchscreen laptop hovers and\n * taps, and iPadOS reports a fine hovering pointer for a finger — so both\n * paths stay live and each handler branches on the event it was given.\n *\n * A pen resting on the glass is making contact, not hovering: `buttons` is the\n * tell, and it sends a pen tap down the same route a finger takes.\n *\n * This answers what an *enter* asks. A leave is the other half of a pair and\n * has to be read against the enter that started it — `useHoverGesture` in\n * `lib/hooks/use-hover-gesture` does that, and hover surfaces should use it\n * rather than asking this question twice.\n */\nexport const isHoveringPointer = (event: {\n  pointerType: string;\n  buttons: number;\n}) => event.pointerType !== \"touch\" && event.buttons === 0;\n"},{"path":"components/motion/input.tsx","type":"registry:component","target":"@components/motion/input.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type InputHTMLAttributes,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type InputClassNames = {\n  root?: string;\n  label?: string;\n  field?: string;\n  input?: string;\n  leftIcon?: string;\n  rightIcon?: string;\n  successIcon?: string;\n  errorMessage?: string;\n};\n\nexport interface InputProps extends Omit<\n  InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> {\n  label?: string;\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  /** Truthy error triggers a shake, red border and (if a string) a message. */\n  error?: string | boolean;\n  success?: boolean;\n  leftIcon?: ReactNode;\n  rightIcon?: ReactNode;\n  className?: string;\n  classNames?: InputClassNames;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n  {\n    label,\n    value: valueProp,\n    defaultValue,\n    onChange,\n    onFocus,\n    onBlur,\n    error,\n    success,\n    leftIcon,\n    rightIcon,\n    className,\n    classNames,\n    disabled,\n    id: idProp,\n    type,\n    ...rest\n  },\n  ref,\n) {\n  const reactId = useId();\n  const id = idProp ?? reactId;\n  const reduce = useReducedMotion();\n\n  const controlled = valueProp !== undefined;\n  const [internal, setInternal] = useState(defaultValue ?? \"\");\n  const value = controlled ? (valueProp ?? \"\") : internal;\n\n  const [focused, setFocused] = useState(false);\n\n  const fieldRef = useRef<HTMLDivElement>(null);\n\n  const hasError = Boolean(error);\n  const errorMessage = typeof error === \"string\" ? error : null;\n\n  // Right edge shows the success check, otherwise the caller's right icon.\n  const rightSlot = success ? null : rightIcon;\n\n  // Shake the field when an error appears.\n  useEffect(() => {\n    if (!fieldRef.current || reduce || !hasError) return;\n    animate(\n      fieldRef.current,\n      { x: [0, -6, 6, -4, 4, -2, 0] },\n      { duration: 0.45 },\n    );\n  }, [hasError, reduce]);\n\n  const handleChange = (next: string) => {\n    if (!controlled) setInternal(next);\n    onChange?.(next);\n  };\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-1.5\", className, classNames?.root)}\n    >\n      {label ? (\n        <label\n          htmlFor={id}\n          className={cn(\n            \"px-1 text-sm font-medium text-foreground\",\n            classNames?.label,\n          )}\n        >\n          {label}\n        </label>\n      ) : null}\n\n      <div\n        ref={fieldRef}\n        data-state={\n          hasError\n            ? \"error\"\n            : success\n              ? \"success\"\n              : focused\n                ? \"focused\"\n                : \"idle\"\n        }\n        className={cn(\n          \"relative h-11 overflow-hidden rounded-full border transition-colors duration-200\",\n          \"border-border\",\n          focused && !hasError && \"border-foreground/40 ring-2 ring-ring/40\",\n          hasError && \"border-destructive ring-2 ring-destructive/25\",\n          disabled && \"opacity-60\",\n          classNames?.field,\n        )}\n      >\n        {leftIcon ? (\n          <span\n            className={cn(\n              \"pointer-events-none absolute left-3 top-1/2 flex -translate-y-1/2 items-center text-muted-foreground [&_svg]:h-4 [&_svg]:w-4\",\n              classNames?.leftIcon,\n            )}\n          >\n            {leftIcon}\n          </span>\n        ) : null}\n\n        <input\n          ref={ref}\n          id={id}\n          type={type}\n          value={value}\n          disabled={disabled}\n          aria-invalid={hasError || undefined}\n          aria-describedby={errorMessage ? `${id}-error` : undefined}\n          {...rest}\n          onChange={(e) => handleChange(e.target.value)}\n          onFocus={(event) => {\n            setFocused(true);\n            onFocus?.(event);\n          }}\n          onBlur={(event) => {\n            setFocused(false);\n            onBlur?.(event);\n          }}\n          className={cn(\n            \"peer h-full w-full bg-transparent text-base leading-6 text-foreground caret-foreground outline-none\",\n            \"placeholder:text-muted-foreground/60\",\n            leftIcon ? \"pl-10\" : \"pl-3.5\",\n            rightSlot || success ? \"pr-10\" : \"pr-3.5\",\n            disabled && \"cursor-not-allowed\",\n            classNames?.input,\n          )}\n        />\n\n        {success ? (\n          <motion.svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            className={cn(\n              \"absolute right-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-(--color-success)\",\n              classNames?.successIcon,\n            )}\n          >\n            <motion.path\n              d=\"M5 12.5l4.5 4.5L19 7.5\"\n              stroke=\"currentColor\"\n              strokeWidth={2.5}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}\n              animate={{ pathLength: 1 }}\n              transition={{ duration: 0.35, ease: \"easeOut\" }}\n            />\n          </motion.svg>\n        ) : rightSlot ? (\n          <span\n            className={cn(\n              \"absolute right-0 top-0 flex h-full items-center text-muted-foreground [&_button]:grid [&_button]:size-11 [&_button]:place-items-center [&_svg]:h-4 [&_svg]:w-4\",\n              classNames?.rightIcon,\n            )}\n          >\n            {rightSlot}\n          </span>\n        ) : null}\n      </div>\n\n      <AnimatePresence initial={false}>\n        {errorMessage ? (\n          <motion.p\n            id={`${id}-error`}\n            role=\"alert\"\n            initial={\n              reduce\n                ? { opacity: 0 }\n                : { opacity: 0, y: -4, filter: \"blur(4px)\" }\n            }\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={\n              reduce\n                ? { opacity: 0 }\n                : { opacity: 0, y: -4, filter: \"blur(4px)\" }\n            }\n            transition={{ duration: 0.2 }}\n            className={cn(\n              \"px-1 text-xs text-destructive\",\n              classNames?.errorMessage,\n            )}\n          >\n            {errorMessage}\n          </motion.p>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n});\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"registry:hook","target":"@lib/hooks/use-hover-capable.ts","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"},{"path":"components/motion/magnetic.tsx","type":"registry:component","target":"@components/motion/magnetic.tsx","content":"\"use client\";\n\nimport { motion, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { SPRING_MOUSE } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MagneticProps {\n  children: ReactNode;\n  strength?: number;\n  className?: string;\n}\n\nexport function Magnetic({ children, strength = 0.35, className }: MagneticProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  // Decorative cursor-follow: skip on touch (phantom hover) and reduced motion.\n  const enabled = !reduce && canHover;\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const sx = useSpring(x, SPRING_MOUSE);\n  const sy = useSpring(y, SPRING_MOUSE);\n\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = ref.current;\n    if (!el || !enabled) return;\n    const rect = el.getBoundingClientRect();\n    x.set((e.clientX - rect.left - rect.width / 2) * strength);\n    y.set((e.clientY - rect.top - rect.height / 2) * strength);\n  };\n\n  const onLeave = () => {\n    x.set(0);\n    y.set(0);\n  };\n\n  return (\n    <motion.div\n      ref={ref}\n      onMouseMove={onMove}\n      onMouseLeave={onLeave}\n      style={{ x: sx, y: sy }}\n      className={cn(\"inline-block\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"}]}