{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "invoice-table",
  "title": "Invoice table",
  "description": "Invoice table is a production-ready tables component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "lucide-react",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/tables/invoice-table.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type KeyboardEvent as ReactKeyboardEvent,\n} from \"react\";\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport { useTheme } from \"next-themes\";\nimport { Plus, Trash2 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { useElementSize } from \"../_core/canvas\";\nimport {\n  type BjorkTableThemeMode,\n  getBjorkTablePalette,\n  useBjorkTableIsDark,\n} from \"./table-theme\";\n\nexport interface InvoiceLine {\n  id: string;\n  description: string;\n  sku?: string;\n  /** Up to two decimals, e.g. 12.5 hours. */\n  quantity: number;\n  /** Price of one unit in minor units (cents). */\n  unitPrice: number;\n  /** Percent off this line, 0–100. */\n  discount?: number;\n  /** Tax percent applied after the discount, e.g. 8.25. */\n  taxRate?: number;\n}\n\nexport interface InvoiceParty {\n  name: string;\n  lines: string[];\n}\n\nexport interface InvoiceMeta {\n  number: string;\n  /** ISO date. */\n  issuedOn: string;\n  /** ISO date. */\n  dueOn: string;\n  from: InvoiceParty;\n  billTo: InvoiceParty;\n}\n\n/** All amounts in integer minor units. */\nexport interface InvoiceTotals {\n  subtotal: number;\n  discount: number;\n  tax: number;\n  total: number;\n}\n\nexport interface InvoiceTableProps {\n  /** Controlled lines. Pair with `onChange`. */\n  lines?: InvoiceLine[];\n  /** Starting lines when uncontrolled. Defaults to the sample invoice. */\n  defaultLines?: InvoiceLine[];\n  onChange?: (lines: InvoiceLine[], totals: InvoiceTotals) => void;\n  /** Inputs for description, quantity, unit price, discount and tax, plus add/remove. Defaults to true. */\n  editable?: boolean;\n  /** ISO 4217 code. Minor-unit digits come from Intl (USD 2, JPY 0). */\n  currency?: string;\n  locale?: string;\n  /** Invoice header. Pass `null` to render only the table. */\n  meta?: InvoiceMeta | null;\n  caption?: string;\n  className?: string;\n  theme?: BjorkTableThemeMode;\n  enableAnimations?: boolean;\n}\n\n/* ------------------------------------------------------------------ */\n/* Sample data                                                         */\n/* ------------------------------------------------------------------ */\n\nexport const INVOICE_SAMPLE_META: InvoiceMeta = {\n  number: \"INV-2026-0419\",\n  issuedOn: \"2026-10-01\",\n  dueOn: \"2026-10-31\",\n  from: { name: \"Halden & Rowe Studio\", lines: [\"418 Tannery Row, Suite 3\", \"Portland, OR 97209\"] },\n  billTo: { name: \"Meridian Coffee Roasters\", lines: [\"Attn: Accounts payable\", \"77 Kiln Street, Duluth, MN 55802\"] },\n};\n\nexport const INVOICE_SAMPLE_LINES: InvoiceLine[] = [\n  {\n    id: \"ln-1\",\n    description: \"Brand identity system\",\n    sku: \"HR-ID-01 · logo suite, type, color tokens\",\n    quantity: 1,\n    unitPrice: 480_000,\n  },\n  {\n    id: \"ln-2\",\n    description: \"Packaging design, retail bags\",\n    sku: \"HR-PK-03 · per SKU\",\n    quantity: 3,\n    unitPrice: 125_000,\n    discount: 10,\n  },\n  {\n    id: \"ln-3\",\n    description: \"Art direction, on-site shoot\",\n    sku: \"HR-AD-HR · hourly\",\n    quantity: 12.5,\n    unitPrice: 14_500,\n  },\n  {\n    id: \"ln-4\",\n    description: \"Printed proofs, 250 gsm\",\n    sku: \"HR-PR-250\",\n    quantity: 40,\n    unitPrice: 675,\n    taxRate: 8.25,\n  },\n  {\n    id: \"ln-5\",\n    description: \"Stock photo license, extended\",\n    sku: \"HR-LC-07\",\n    quantity: 2,\n    unitPrice: 8_999,\n    discount: 5,\n    taxRate: 8.25,\n  },\n];\n\n/* ------------------------------------------------------------------ */\n/* Integer money math                                                  */\n/* ------------------------------------------------------------------ */\n\n/** Percent (e.g. 8.25) to basis points (825) without float drift. */\nconst toBps = (percent = 0) => Math.round(percent * 100);\n/** Divide and round half away from zero, on integers. */\nconst divRound = (numerator: number, denominator: number) =>\n  Math.sign(numerator) * Math.round(Math.abs(numerator) / denominator);\n\nexport function computeInvoiceLine(line: InvoiceLine) {\n  const qtyHundredths = Math.round(line.quantity * 100);\n  const gross = divRound(qtyHundredths * line.unitPrice, 100);\n  const discount = divRound(gross * toBps(line.discount), 10_000);\n  const net = gross - discount;\n  const tax = divRound(net * toBps(line.taxRate), 10_000);\n  return { gross, discount, net, tax };\n}\n\nexport function computeInvoiceTotals(lines: InvoiceLine[]): InvoiceTotals {\n  let subtotal = 0;\n  let discount = 0;\n  let tax = 0;\n  for (const line of lines) {\n    const amounts = computeInvoiceLine(line);\n    subtotal += amounts.gross;\n    discount += amounts.discount;\n    tax += amounts.tax;\n  }\n  return { subtotal, discount, tax, total: subtotal - discount + tax };\n}\n\n/* ------------------------------------------------------------------ */\n/* Component                                                           */\n/* ------------------------------------------------------------------ */\n\nconst noopSubscribe = () => () => {};\nconst focusRing = \"outline-none focus-visible:ring-2 focus-visible:ring-[#ec5c13]/45\";\nlet lineCounter = 0;\n\nexport function InvoiceTable({\n  lines: controlledLines,\n  defaultLines = INVOICE_SAMPLE_LINES,\n  onChange,\n  editable = true,\n  currency = \"USD\",\n  locale = \"en-US\",\n  meta = INVOICE_SAMPLE_META,\n  caption = \"Invoice line items\",\n  className,\n  theme = \"auto\",\n  enableAnimations = true,\n}: InvoiceTableProps) {\n  const mounted = useSyncExternalStore(noopSubscribe, () => true, () => false);\n  const { resolvedTheme } = useTheme();\n  const forcedTheme = theme === \"auto\" ? undefined : theme;\n  const detectedIsDark = useBjorkTableIsDark(resolvedTheme, forcedTheme);\n  const isDark = !mounted && theme === \"auto\" ? true : detectedIsDark;\n  const palette = getBjorkTablePalette(isDark);\n  const reduceMotion = useReducedMotion();\n  const shouldAnimate = enableAnimations && !reduceMotion;\n\n  const [internalLines, setInternalLines] = useState(defaultLines);\n  const lines = controlledLines ?? internalLines;\n  const [entered, setEntered] = useState(false);\n  const [lastAdded, setLastAdded] = useState<{ id: string; instant: boolean } | null>(null);\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const { width } = useElementSize(rootRef);\n  const compact = width > 0 && width < 640;\n\n  const uid = useId();\n  const tableRef = useRef<HTMLTableElement>(null);\n  const addButtonRef = useRef<HTMLButtonElement>(null);\n  const pendingFocus = useRef<{ kind: \"description\"; id: string } | { kind: \"remove\"; index: number } | null>(null);\n\n  useEffect(() => {\n    const id = window.setTimeout(() => setEntered(true), 50);\n    return () => window.clearTimeout(id);\n  }, []);\n\n  const money = useMemo(() => {\n    const currencyFormat = new Intl.NumberFormat(locale, { style: \"currency\", currency });\n    const digits = currencyFormat.resolvedOptions().maximumFractionDigits ?? 2;\n    const plain = new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits: digits });\n    const parts = new Intl.NumberFormat(locale).formatToParts(1.5);\n    const decimal = parts.find((p) => p.type === \"decimal\")?.value ?? \".\";\n    const scale = 10 ** digits;\n    return {\n      digits,\n      decimal,\n      format: (minor: number) => currencyFormat.format(minor / scale),\n      plain: (minor: number) => plain.format(minor / scale),\n      /** Parse user text into integer minor units using string math only. */\n      parse: (text: string): number | null => {\n        const cleaned = text.replace(new RegExp(`[^0-9\\\\${decimal}-]`, \"g\"), \"\");\n        if (!cleaned || cleaned === \"-\") return null;\n        const negative = cleaned.startsWith(\"-\");\n        const [wholeRaw, fracRaw = \"\"] = cleaned.replace(/-/g, \"\").split(decimal);\n        const whole = Number(wholeRaw || \"0\");\n        const padded = (fracRaw + \"0\".repeat(digits + 1)).slice(0, digits + 1);\n        const frac = digits === 0 ? 0 : Number(padded.slice(0, digits));\n        const roundUp = Number(padded[digits] ?? \"0\") >= 5 ? 1 : 0;\n        const value = whole * scale + frac + roundUp;\n        if (!Number.isFinite(value)) return null;\n        return negative ? -value : value;\n      },\n    };\n  }, [locale, currency]);\n\n  const numberFormat = useMemo(\n    () => new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }),\n    [locale]\n  );\n  const dateFormat = useMemo(\n    () => new Intl.DateTimeFormat(locale, { month: \"short\", day: \"numeric\", year: \"numeric\", timeZone: \"UTC\" }),\n    [locale]\n  );\n\n  const totals = useMemo(() => computeInvoiceTotals(lines), [lines]);\n  const taxRates = useMemo(\n    () => [...new Set(lines.filter((l) => l.taxRate).map((l) => l.taxRate as number))],\n    [lines]\n  );\n\n  const commit = useCallback(\n    (next: InvoiceLine[]) => {\n      if (controlledLines === undefined) setInternalLines(next);\n      onChange?.(next, computeInvoiceTotals(next));\n    },\n    [controlledLines, onChange]\n  );\n\n  const updateLine = (id: string, patch: Partial<InvoiceLine>) => {\n    commit(lines.map((line) => (line.id === id ? { ...line, ...patch } : line)));\n  };\n\n  const addLine = (instant: boolean) => {\n    lineCounter += 1;\n    const id = `ln-new-${Date.now().toString(36)}-${lineCounter}`;\n    pendingFocus.current = { kind: \"description\", id };\n    setLastAdded({ id, instant });\n    commit([...lines, { id, description: \"\", sku: \"\", quantity: 1, unitPrice: 0 }]);\n  };\n\n  const removeLine = (index: number) => {\n    pendingFocus.current = { kind: \"remove\", index };\n    commit(lines.filter((_, i) => i !== index));\n  };\n\n  // Move focus after add/remove so keyboard users never land on <body>.\n  useEffect(() => {\n    const target = pendingFocus.current;\n    if (!target) return;\n    pendingFocus.current = null;\n    const table = tableRef.current;\n    if (!table) return;\n    if (target.kind === \"description\") {\n      table.querySelector<HTMLInputElement>(`[data-line=\"${target.id}\"][data-field=\"description\"]`)?.focus();\n      return;\n    }\n    const removers = table.querySelectorAll<HTMLButtonElement>(\"[data-remove]\");\n    const next = removers[Math.min(target.index, removers.length - 1)];\n    (next ?? addButtonRef.current)?.focus();\n  }, [lines]);\n\n  const fieldTone = isDark\n    ? \"border-[#232323] bg-[#181818] text-[#ededed]/90 hover:border-[#2e2e2e] placeholder:text-[#ededed]/26\"\n    : \"border-[#eadfce] bg-[#fffcf6] text-[#171717]/86 hover:border-[#dccdb6] placeholder:text-[#171717]/36\";\n  const fieldBase = cn(\n    \"h-8 w-full rounded-[8px] border px-2 text-[13px] transition-colors focus:border-[#ec5c13]/50\",\n    \"print:h-auto print:border-transparent print:bg-transparent print:px-0 print:text-inherit\",\n    fieldTone,\n    focusRing\n  );\n  const bareField = cn(\n    \"h-7 w-full rounded-[7px] border border-transparent bg-transparent px-1.5 text-[13px] transition-colors\",\n    isDark ? \"hover:border-[#2a2a2a] hover:bg-[#181818]\" : \"hover:border-[#eadfce] hover:bg-[#fffcf6]\",\n    \"focus:border-[#ec5c13]/50 print:border-transparent print:bg-transparent print:px-0\",\n    focusRing\n  );\n\n  // Column plan: item | qty | unit | disc | tax | amount | remove\n  const showAction = editable;\n  const midColumns = compact ? 0 : 4; // qty, unit, disc, tax (folded under the item when compact)\n  const allColumns = 1 + midColumns + 1 + (showAction ? 1 : 0);\n  const headerCell = cn(\"sticky top-0 z-[1] h-9 border-b px-2 text-[11px] font-medium uppercase tracking-[0.08em]\", palette.header);\n  const animateIn = shouldAnimate && !entered;\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\n        \"mx-auto w-full max-w-[920px] text-[13px] print:max-w-none print:text-black\",\n        // Paper is always light: force ink, hairlines and clear fills regardless of theme.\n        \"print:**:border-black/15! print:**:bg-transparent! print:**:text-black! print:**:shadow-none! print:**:transition-none!\",\n        className\n      )}\n    >\n      <div\n        className={cn(\n          \"overflow-hidden rounded-[18px] border print:rounded-none print:border-0 print:bg-transparent print:shadow-none\",\n          palette.container\n        )}\n      >\n        {meta ? (\n          <header className={cn(\"grid gap-4 border-b px-5 pb-4 pt-5\", palette.divider, compact ? \"grid-cols-1\" : \"grid-cols-[1.2fr_1fr_1fr]\")}>\n            <div className=\"min-w-0\">\n              <p className={cn(\"text-[11px] font-medium uppercase tracking-[0.08em]\", palette.secondaryText)}>Invoice</p>\n              <p className={cn(\"font-mono text-[13px]\", palette.primaryText)}>{meta.number}</p>\n              <dl className=\"mt-3 flex gap-5 text-[12px]\">\n                <div>\n                  <dt className={palette.secondaryText}>Issued</dt>\n                  <dd className={cn(\"tabular-nums\", palette.primaryText)}>\n                    <time dateTime={meta.issuedOn}>{dateFormat.format(new Date(meta.issuedOn))}</time>\n                  </dd>\n                </div>\n                <div>\n                  <dt className={palette.secondaryText}>Due</dt>\n                  <dd className={cn(\"tabular-nums\", palette.primaryText)}>\n                    <time dateTime={meta.dueOn}>{dateFormat.format(new Date(meta.dueOn))}</time>\n                  </dd>\n                </div>\n              </dl>\n            </div>\n            <div className={cn(compact && \"grid grid-cols-2 gap-4\", !compact && \"contents\")}>\n              {[\n                { label: \"From\", party: meta.from },\n                { label: \"Bill to\", party: meta.billTo },\n              ].map(({ label, party }) => (\n                <address key={label} className=\"min-w-0 text-[12px] not-italic leading-relaxed\">\n                  <span className={cn(\"block text-[11px] font-medium uppercase tracking-[0.08em]\", palette.secondaryText)}>\n                    {label}\n                  </span>\n                  <span className={cn(\"mt-1 block text-[13px] font-medium\", palette.primaryText)}>{party.name}</span>\n                  {party.lines.map((l) => (\n                    <span key={l} className={cn(\"block\", palette.secondaryText)}>\n                      {l}\n                    </span>\n                  ))}\n                </address>\n              ))}\n            </div>\n          </header>\n        ) : null}\n\n        <div\n          role=\"region\"\n          aria-label={caption}\n          tabIndex={0}\n          className={cn(\"overflow-x-auto\", focusRing, \"focus-visible:ring-inset\")}\n        >\n          <table ref={tableRef} className=\"w-full table-fixed border-separate border-spacing-0\">\n            <caption className=\"sr-only\">\n              {caption}\n              {meta ? `, invoice ${meta.number}` : \"\"}. Total {money.format(totals.total)}.\n            </caption>\n            <colgroup>\n              <col />\n              {compact ? null : (\n                <>\n                  <col style={{ width: 84 }} />\n                  <col style={{ width: 120 }} />\n                  <col style={{ width: 78 }} />\n                  <col style={{ width: 78 }} />\n                </>\n              )}\n              <col style={{ width: compact ? 104 : 124 }} />\n              {showAction ? <col className=\"print:hidden\" style={{ width: 44 }} /> : null}\n            </colgroup>\n            <thead>\n              <tr>\n                <th scope=\"col\" className={cn(headerCell, \"pl-5 text-left\")}>\n                  Item\n                </th>\n                {compact ? null : (\n                  <>\n                    <th scope=\"col\" className={cn(headerCell, \"text-right\")}>\n                      Qty\n                    </th>\n                    <th scope=\"col\" className={cn(headerCell, \"text-right\")}>\n                      Unit price\n                    </th>\n                    <th scope=\"col\" className={cn(headerCell, \"text-right\")}>\n                      <abbr title=\"Discount\" className=\"no-underline\">Disc.</abbr>\n                    </th>\n                    <th scope=\"col\" className={cn(headerCell, \"text-right\")}>\n                      Tax\n                    </th>\n                  </>\n                )}\n                <th scope=\"col\" className={cn(headerCell, \"text-right\", !showAction && \"pr-5\")}>\n                  Amount\n                </th>\n                {showAction ? (\n                  <th scope=\"col\" className={cn(headerCell, \"print:hidden\")}>\n                    <span className=\"sr-only\">Actions</span>\n                  </th>\n                ) : null}\n              </tr>\n            </thead>\n            <tbody>\n              {lines.length === 0 ? (\n                <tr>\n                  <td colSpan={allColumns} className=\"px-6 py-12 text-center\">\n                    <p className={cn(\"text-[14px] font-medium\", palette.primaryText)}>No line items yet</p>\n                    <p className={cn(\"mt-1 text-[12px]\", palette.secondaryText)}>\n                      {editable ? \"Add a line to start building this invoice.\" : \"This invoice has no billable items.\"}\n                    </p>\n                  </td>\n                </tr>\n              ) : null}\n              {lines.map((line, index) => {\n                const amounts = computeInvoiceLine(line);\n                const label = line.description || `Line ${index + 1}`;\n                const cell = cn(\"border-b px-2 py-2.5 align-top\", palette.divider);\n                const isNew = lastAdded?.id === line.id;\n                const rowInitial =\n                  (animateIn || (isNew && shouldAnimate && !lastAdded?.instant)) ? { opacity: 0, y: 6 } : false;\n                return (\n                  <motion.tr\n                    key={line.id}\n                    initial={rowInitial}\n                    animate={{ opacity: 1, y: 0 }}\n                    transition={{ type: \"spring\", stiffness: 420, damping: 34, delay: animateIn ? index * 0.04 : 0 }}\n                    className={cn(\"transition-colors duration-150\", palette.row)}\n                  >\n                    <td className={cn(cell, \"pl-5\")}>\n                      {editable ? (\n                        <div className=\"flex min-w-0 flex-col gap-0.5\">\n                          <input\n                            data-line={line.id}\n                            data-field=\"description\"\n                            value={line.description}\n                            placeholder=\"Description\"\n                            aria-label={`Line ${index + 1} description`}\n                            onChange={(event) => updateLine(line.id, { description: event.target.value })}\n                            className={cn(bareField, \"-ml-1.5 font-medium\", palette.primaryText)}\n                          />\n                          <input\n                            value={line.sku ?? \"\"}\n                            placeholder=\"SKU or note\"\n                            aria-label={`Line ${index + 1} SKU`}\n                            onChange={(event) => updateLine(line.id, { sku: event.target.value })}\n                            className={cn(bareField, \"-ml-1.5 h-6 font-mono text-[11px]\", palette.secondaryText)}\n                          />\n                        </div>\n                      ) : (\n                        <div className=\"flex min-w-0 flex-col gap-0.5 pt-1\">\n                          <span className={cn(\"truncate font-medium\", palette.primaryText)}>{line.description}</span>\n                          {line.sku ? (\n                            <span className={cn(\"truncate font-mono text-[11px]\", palette.secondaryText)}>{line.sku}</span>\n                          ) : null}\n                        </div>\n                      )}\n                      {compact ? (\n                        <div className={cn(\"mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[12px] tabular-nums\", palette.secondaryText)}>\n                          <span className=\"flex items-center gap-1\">\n                            {editable ? (\n                              <NumberField\n                                value={line.quantity}\n                                display={(v) => numberFormat.format(v)}\n                                parse={(text) => parseQuantity(text, money.decimal)}\n                                step={1}\n                                min={0}\n                                ariaLabel={`Line ${index + 1} quantity`}\n                                describedBy={`${uid}-hint`}\n                                onCommit={(quantity) => updateLine(line.id, { quantity })}\n                                className={cn(fieldBase, \"h-7 w-[52px] text-right tabular-nums\")}\n                              />\n                            ) : (\n                              numberFormat.format(line.quantity)\n                            )}\n                            <span aria-hidden>×</span>\n                            {editable ? (\n                              <NumberField\n                                value={line.unitPrice}\n                                display={money.plain}\n                                parse={money.parse}\n                                step={10 ** money.digits}\n                                min={0}\n                                ariaLabel={`Line ${index + 1} unit price`}\n                                describedBy={`${uid}-hint`}\n                                onCommit={(unitPrice) => updateLine(line.id, { unitPrice })}\n                                className={cn(fieldBase, \"h-7 w-[92px] text-right tabular-nums\")}\n                              />\n                            ) : (\n                              money.format(line.unitPrice)\n                            )}\n                          </span>\n                          {line.discount ? <span>−{numberFormat.format(line.discount)}%</span> : null}\n                          {line.taxRate ? <span>+{numberFormat.format(line.taxRate)}% tax</span> : null}\n                        </div>\n                      ) : null}\n                    </td>\n                    {compact ? null : (\n                      <>\n                        <td className={cn(cell, \"text-right tabular-nums\")}>\n                          {editable ? (\n                            <NumberField\n                              value={line.quantity}\n                              display={(v) => numberFormat.format(v)}\n                              parse={(text) => parseQuantity(text, money.decimal)}\n                              step={1}\n                              min={0}\n                              ariaLabel={`Line ${index + 1} quantity`}\n                              describedBy={`${uid}-hint`}\n                              onCommit={(quantity) => updateLine(line.id, { quantity })}\n                              className={cn(fieldBase, \"text-right tabular-nums\")}\n                            />\n                          ) : (\n                            <span className={cn(\"block pt-1\", palette.primaryText)}>{numberFormat.format(line.quantity)}</span>\n                          )}\n                        </td>\n                        <td className={cn(cell, \"text-right tabular-nums\")}>\n                          {editable ? (\n                            <NumberField\n                              value={line.unitPrice}\n                              display={money.plain}\n                              parse={money.parse}\n                              step={10 ** money.digits}\n                              min={0}\n                              ariaLabel={`Line ${index + 1} unit price`}\n                              describedBy={`${uid}-hint`}\n                              onCommit={(unitPrice) => updateLine(line.id, { unitPrice })}\n                              className={cn(fieldBase, \"text-right tabular-nums\")}\n                            />\n                          ) : (\n                            <span className={cn(\"block pt-1\", palette.primaryText)}>{money.format(line.unitPrice)}</span>\n                          )}\n                        </td>\n                        <td className={cn(cell, \"text-right tabular-nums\")}>\n                          {editable ? (\n                            <NumberField\n                              value={line.discount ?? 0}\n                              display={(v) => (v ? `${numberFormat.format(v)}%` : \"—\")}\n                              parse={(text) => clampPercent(parseDecimal(text, money.decimal))}\n                              step={1}\n                              min={0}\n                              max={100}\n                              ariaLabel={`Line ${index + 1} discount percent`}\n                              describedBy={`${uid}-hint`}\n                          onCommit={(discount) => updateLine(line.id, { discount })}\n                              className={cn(bareField, \"h-8 text-right tabular-nums\", line.discount ? palette.accent : palette.secondaryText)}\n                            />\n                          ) : (\n                            <span className={cn(\"block pt-1\", line.discount ? palette.accent : palette.secondaryText)}>\n                              {line.discount ? `${numberFormat.format(line.discount)}%` : \"—\"}\n                            </span>\n                          )}\n                        </td>\n                        <td className={cn(cell, \"text-right tabular-nums\")}>\n                          {editable ? (\n                            <NumberField\n                              value={line.taxRate ?? 0}\n                              display={(v) => (v ? `${numberFormat.format(v)}%` : \"—\")}\n                              parse={(text) => clampPercent(parseDecimal(text, money.decimal))}\n                              step={0.25}\n                              min={0}\n                              max={100}\n                              ariaLabel={`Line ${index + 1} tax percent`}\n                              describedBy={`${uid}-hint`}\n                          onCommit={(taxRate) => updateLine(line.id, { taxRate })}\n                              className={cn(bareField, \"h-8 text-right tabular-nums\", line.taxRate ? palette.primaryText : palette.secondaryText)}\n                            />\n                          ) : (\n                            <span className={cn(\"block pt-1\", line.taxRate ? palette.primaryText : palette.secondaryText)}>\n                              {line.taxRate ? `${numberFormat.format(line.taxRate)}%` : \"—\"}\n                            </span>\n                          )}\n                        </td>\n                      </>\n                    )}\n                    <td className={cn(cell, \"text-right tabular-nums\", !showAction && \"pr-5\")}>\n                      <span className={cn(\"block pt-1.5 font-medium\", palette.primaryText)}>{money.format(amounts.net)}</span>\n                      {amounts.discount ? (\n                        <span className={cn(\"block text-[11px] line-through decoration-1\", palette.secondaryText)}>\n                          {money.format(amounts.gross)}\n                        </span>\n                      ) : null}\n                    </td>\n                    {showAction ? (\n                      <td className={cn(cell, \"px-0 text-center print:hidden\")}>\n                        <button\n                          type=\"button\"\n                          data-remove\n                          aria-label={`Remove ${label}`}\n                          onClick={() => removeLine(index)}\n                          className={cn(\n                            \"mt-0.5 inline-flex h-7 w-7 items-center justify-center rounded-[8px] transition-colors\",\n                            palette.secondaryText,\n                            isDark ? \"hover:bg-[#87463f]/18 hover:text-[#cf7c72]\" : \"hover:bg-[#eadad5] hover:text-[#9d5149]\",\n                            focusRing\n                          )}\n                        >\n                          <Trash2 aria-hidden className=\"h-3.5 w-3.5\" />\n                        </button>\n                      </td>\n                    ) : null}\n                  </motion.tr>\n                );\n              })}\n            </tbody>\n            <tfoot className=\"tabular-nums\">\n              {editable ? (\n                <tr className=\"print:hidden\">\n                  <td colSpan={allColumns} className={cn(\"border-b px-5 py-2.5\", palette.divider)}>\n                    <button\n                      ref={addButtonRef}\n                      type=\"button\"\n                      onClick={(event) => addLine(event.detail === 0)}\n                      className={cn(\n                        \"inline-flex h-8 items-center gap-1.5 rounded-[9px] border border-dashed px-2.5 text-[13px] transition-colors\",\n                        palette.accentBorder,\n                        palette.accent,\n                        isDark ? \"hover:bg-[#ec5c13]/10\" : \"hover:bg-[#ec5c13]/8\",\n                        focusRing\n                      )}\n                    >\n                      <Plus aria-hidden className=\"h-3.5 w-3.5\" />\n                      Add line\n                    </button>\n                  </td>\n                </tr>\n              ) : null}\n              <TotalRow\n                label=\"Subtotal\"\n                value={money.format(totals.subtotal)}\n                compact={compact}\n                midColumns={midColumns}\n                showAction={showAction}\n                palette={palette}\n                first\n              />\n              {totals.discount ? (\n                <TotalRow\n                  label=\"Discounts\"\n                  value={money.format(-totals.discount)}\n                  compact={compact}\n                  midColumns={midColumns}\n                  showAction={showAction}\n                  palette={palette}\n                  valueClassName={palette.accent}\n                />\n              ) : null}\n              <TotalRow\n                label={taxRates.length === 1 ? `Tax (${numberFormat.format(taxRates[0])}%)` : \"Tax\"}\n                value={money.format(totals.tax)}\n                compact={compact}\n                midColumns={midColumns}\n                showAction={showAction}\n                palette={palette}\n              />\n              <TotalRow\n                label={`Total due (${currency})`}\n                value={money.format(totals.total)}\n                compact={compact}\n                midColumns={midColumns}\n                showAction={showAction}\n                palette={palette}\n                emphasis\n                live\n              />\n            </tfoot>\n          </table>\n        </div>\n      </div>\n      <span id={`${uid}-hint`} className=\"sr-only\">\n        Use the up and down arrow keys to step a number field. Press Enter to apply, Escape to undo.\n      </span>\n    </div>\n  );\n}\n\nfunction TotalRow({\n  label,\n  value,\n  compact,\n  midColumns,\n  showAction,\n  palette,\n  valueClassName,\n  emphasis = false,\n  first = false,\n  live = false,\n}: {\n  label: string;\n  value: string;\n  compact: boolean;\n  midColumns: number;\n  showAction: boolean;\n  palette: ReturnType<typeof getBjorkTablePalette>;\n  valueClassName?: string;\n  emphasis?: boolean;\n  first?: boolean;\n  live?: boolean;\n}) {\n  const pad = first ? \"pt-3.5\" : emphasis ? \"pt-3 pb-4\" : \"pt-1.5\";\n  return (\n    <tr>\n      {compact ? null : <td className={pad} />}\n      <th\n        scope=\"row\"\n        colSpan={compact ? 1 + midColumns : midColumns}\n        className={cn(\n          \"px-2 text-right font-normal\",\n          pad,\n          compact && \"pl-5\",\n          emphasis ? cn(\"text-[13px] font-medium\", palette.primaryText) : cn(\"text-[12px]\", palette.secondaryText)\n        )}\n      >\n        {emphasis ? (\n          <span className={cn(\"block border-t pt-3\", palette.divider)} style={{ marginTop: -1 }}>\n            {label}\n          </span>\n        ) : (\n          label\n        )}\n      </th>\n      <td\n        aria-live={live ? \"polite\" : undefined}\n        className={cn(\n          \"px-2 text-right\",\n          pad,\n          !showAction && \"pr-5\",\n          emphasis ? cn(\"text-[17px] font-semibold tracking-tight\", palette.primaryText) : cn(\"text-[13px]\", valueClassName ?? palette.primaryText)\n        )}\n      >\n        {emphasis ? (\n          <span className={cn(\"block border-t pt-[9px]\", palette.divider)} style={{ marginTop: -1 }}>\n            {value}\n          </span>\n        ) : (\n          value\n        )}\n      </td>\n      {showAction ? <td className={cn(pad, \"print:hidden\")} /> : null}\n    </tr>\n  );\n}\n\nfunction parseDecimal(text: string, decimal: string): number | null {\n  const cleaned = text.replace(new RegExp(`[^0-9\\\\${decimal}]`, \"g\"), \"\").replace(decimal, \".\");\n  if (!cleaned || cleaned === \".\") return null;\n  const value = Number(cleaned);\n  return Number.isFinite(value) ? value : null;\n}\n\nfunction parseQuantity(text: string, decimal: string) {\n  const value = parseDecimal(text, decimal);\n  return value === null ? null : Math.max(0, Math.round(value * 100) / 100);\n}\n\nfunction clampPercent(value: number | null) {\n  if (value === null) return 0;\n  return Math.min(100, Math.max(0, Math.round(value * 100) / 100));\n}\n\n/**\n * Text input for numbers: shows the formatted value, edits a free-form draft,\n * commits on Enter or blur, reverts on Escape, and steps with the arrow keys.\n */\nfunction NumberField({\n  value,\n  display,\n  parse,\n  onCommit,\n  step,\n  min = -Infinity,\n  max = Infinity,\n  ariaLabel,\n  describedBy,\n  className,\n}: {\n  value: number;\n  display: (value: number) => string;\n  parse: (text: string) => number | null;\n  onCommit: (value: number) => void;\n  step: number;\n  min?: number;\n  max?: number;\n  ariaLabel: string;\n  describedBy?: string;\n  className?: string;\n}) {\n  const [draft, setDraft] = useState<string | null>(null);\n  const clamp = (v: number) => Math.min(max, Math.max(min, v));\n\n  const commitDraft = () => {\n    if (draft === null) return;\n    const parsed = parse(draft);\n    setDraft(null);\n    if (parsed !== null && parsed !== value) onCommit(clamp(parsed));\n  };\n\n  const onKeyDown = (event: ReactKeyboardEvent<HTMLInputElement>) => {\n    if (event.key === \"Enter\") {\n      event.preventDefault();\n      commitDraft();\n    } else if (event.key === \"Escape\") {\n      if (draft !== null) {\n        event.preventDefault();\n        setDraft(null);\n      }\n    } else if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n      event.preventDefault();\n      const base = draft !== null ? (parse(draft) ?? value) : value;\n      const delta = (event.key === \"ArrowUp\" ? 1 : -1) * step * (event.shiftKey ? 10 : 1);\n      // Step in integer space so 0.25 increments never drift.\n      const next = clamp(Math.round((base + delta) * 100) / 100);\n      setDraft(null);\n      if (next !== value) onCommit(next);\n    }\n  };\n\n  return (\n    <input\n      type=\"text\"\n      inputMode=\"decimal\"\n      autoComplete=\"off\"\n      spellCheck={false}\n      aria-label={ariaLabel}\n      aria-describedby={describedBy}\n      value={draft ?? display(value)}\n      onFocus={(event) => {\n        setDraft(display(value).replace(/[^0-9.,-]/g, \"\"));\n        const input = event.currentTarget;\n        requestAnimationFrame(() => input.select());\n      }}\n      onChange={(event) => setDraft(event.target.value)}\n      onBlur={commitDraft}\n      onKeyDown={onKeyDown}\n      className={className}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/tables/invoice-table.tsx"
    },
    {
      "path": "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",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    },
    {
      "path": "components/bjork-ui/_core/canvas.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState, type RefObject } from \"react\";\nimport { isCoarsePointer } from \"./loop\";\n\n// Caps DPR at 1.5 on coarse pointers and 2 otherwise.\nexport function defaultMaxDpr(): number {\n  return isCoarsePointer() ? 1.5 : 2;\n}\n\n// Sets the backing size for a CSS box and returns the DPR used.\nexport function sizeCanvas(\n  canvas: HTMLCanvasElement,\n  cssW: number,\n  cssH: number,\n  maxDpr: number = defaultMaxDpr(),\n): number {\n  const dpr = Math.min(typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1, maxDpr);\n  canvas.width = Math.max(1, Math.round(cssW * dpr));\n  canvas.height = Math.max(1, Math.round(cssH * dpr));\n  canvas.style.width = `${cssW}px`;\n  canvas.style.height = `${cssH}px`;\n  return dpr;\n}\n\n// Rounded size of an element, kept current with a ResizeObserver.\nexport function useElementSize(ref: RefObject<HTMLElement | null>): { width: number; height: number } {\n  const [size, setSize] = useState({ width: 0, height: 0 });\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver((entries) => {\n      const box = entries[0]?.contentRect;\n      if (!box) return;\n      const width = Math.round(box.width);\n      const height = Math.round(box.height);\n      setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref]);\n\n  return size;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/canvas.ts"
    },
    {
      "path": "components/bjork-ui/_core/loop.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, type RefObject } from \"react\";\n\nexport interface VisibleLoopOptions {\n  enabled?: boolean;\n  fpsCap?: number;\n  rootMargin?: string;\n}\n\n/**\n * Runs `frame(dt, t)` on requestAnimationFrame while the element is near the viewport,\n * the tab is visible and `enabled` is not false. Return `false` from `frame` to go idle\n * until `wake()` is called. Writes `data-loop` on the element: running, idle or paused.\n */\nexport function useVisibleLoop(\n  ref: RefObject<HTMLElement | null>,\n  frame: (dt: number, t: number) => boolean | void,\n  opts?: VisibleLoopOptions,\n): { wake: () => void } {\n  const enabled = opts?.enabled !== false;\n  const fpsCap = opts?.fpsCap ?? 0;\n  const rootMargin = opts?.rootMargin ?? \"96px\";\n\n  const frameRef = useRef(frame);\n  useEffect(() => {\n    frameRef.current = frame;\n  }, [frame]);\n\n  const wakeRef = useRef<() => void>(() => {});\n  const wake = useCallback(() => wakeRef.current(), []);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof window === \"undefined\") return;\n\n    let wants = true; // false once the frame callback returns false\n    let inView = false;\n    let raf = 0;\n    let last = 0;\n    let t = 0;\n    const minFrame = fpsCap > 0 ? 1000 / fpsCap : 0;\n\n    const canRun = () => enabled && inView && document.visibilityState === \"visible\";\n    const stop = () => {\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n    };\n\n    const update = () => {\n      if (!enabled) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      if (!wants) {\n        stop();\n        el.dataset.loop = \"idle\";\n        return;\n      }\n      if (!canRun()) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      el.dataset.loop = \"running\";\n      if (!raf) {\n        last = performance.now();\n        raf = requestAnimationFrame(tick);\n      }\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      if (!canRun() || !wants) {\n        update();\n        return;\n      }\n      const elapsed = now - last;\n      if (minFrame && elapsed < minFrame - 0.5) {\n        raf = requestAnimationFrame(tick);\n        return;\n      }\n      last = now;\n      const dt = Math.min(0.05, Math.max(0, elapsed / 1000));\n      t += dt;\n      const keep = frameRef.current(dt, t);\n      if (keep === false) {\n        wants = false;\n        update();\n        return;\n      }\n      raf = requestAnimationFrame(tick);\n    };\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        inView = entry.isIntersecting;\n        update();\n      },\n      { rootMargin },\n    );\n    observer.observe(el);\n\n    const onVisibility = () => update();\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    wakeRef.current = () => {\n      wants = true;\n      update();\n    };\n\n    update();\n\n    return () => {\n      stop();\n      observer.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      wakeRef.current = () => {};\n    };\n  }, [ref, enabled, fpsCap, rootMargin]);\n\n  return useMemo(() => ({ wake }), [wake]);\n}\n\n// matchMedia based check. False on the server.\nexport function isCoarsePointer(): boolean {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return false;\n  return window.matchMedia(\"(pointer: coarse)\").matches;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/loop.ts"
    },
    {
      "path": "components/bjork-ui/tables/table-theme.ts",
      "content": "import { useEffect, useState } from \"react\";\n\nexport type BjorkTableThemeMode = \"light\" | \"dark\" | \"auto\";\n\nexport function useBjorkTableIsDark(\n  resolvedTheme?: string,\n  forcedTheme?: Exclude<BjorkTableThemeMode, \"auto\">\n) {\n  const [documentTheme, setDocumentTheme] = useState<\"light\" | \"dark\" | null>(null);\n  const [previewTheme, setPreviewTheme] = useState<\"light\" | \"dark\" | null>(null);\n  // Server and first client render must agree, otherwise React keeps the\n  // server-rendered (dark) classes when the real theme resolves to light.\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n    const syncDocumentTheme = () => {\n      const root = document.documentElement;\n      const searchParams = new URLSearchParams(window.location.search);\n      const theme = searchParams.get(\"theme\");\n\n      setPreviewTheme(theme === \"light\" || theme === \"dark\" ? theme : null);\n\n      setDocumentTheme(\n        root.classList.contains(\"light\")\n          ? \"light\"\n          : root.classList.contains(\"dark\")\n            ? \"dark\"\n            : null\n      );\n    };\n\n    syncDocumentTheme();\n\n    const observer = new MutationObserver(syncDocumentTheme);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    return () => observer.disconnect();\n  }, []);\n\n  const activeTheme =\n    forcedTheme ?? previewTheme ?? documentTheme ?? (mounted ? resolvedTheme : undefined);\n\n  return activeTheme !== \"light\";\n}\n\nexport function getBjorkTablePalette(isDark: boolean) {\n  return {\n    container: isDark\n      ? \"border-[#1c1c1c] bg-[#121212] shadow-[inset_0_7px_14px_rgba(255,255,255,0.03),inset_0_0.5px_0.5px_rgba(255,255,255,0.06),0_14px_20px_-6px_rgba(0,0,0,0.45)]\"\n      : \"border-[#f5ede2] bg-[#fffcf6] shadow-[inset_0_7px_14px_rgba(88,72,49,0.045),inset_0_0.5px_0.5px_rgba(255,255,255,0.92),inset_1px_0_0_rgba(88,72,49,0.026),inset_-1px_0_0_rgba(255,255,255,0.68),0_14px_22px_-9px_rgba(66,52,33,0.11)]\",\n    header: isDark\n      ? \"border-[#232323] bg-[#181818] text-[#ededed]/42\"\n      : \"border-[#f1e8dc] bg-[#fbf7ef] text-[#171717]/60\",\n    row: isDark\n      ? \"border-[#232323] hover:bg-[#181818]/70\"\n      : \"border-[#f1e8dc] hover:bg-[#f8f2e7]/72\",\n    selectedRow: isDark\n      ? \"border-[#2a2a2a] bg-[#181818]\"\n      : \"border-[#eadfce] bg-[#f8f2e7]\",\n    control: isDark\n      ? \"border-[#232323] bg-[#181818] text-[#ededed]/70 hover:bg-[#202020] shadow-[inset_0_1px_0_rgba(255,255,255,0.045)]\"\n      : \"border-[#eadfce] bg-[#fffcf6] text-[#171717]/68 shadow-[var(--bjork-shadow-soft)] hover:bg-[#f8f2e7]\",\n    menu: isDark\n      ? \"border-[#232323] bg-[#181818] text-[#ededed]/76 shadow-[inset_0_1px_0_rgba(255,255,255,0.055),inset_0_12px_24px_rgba(255,255,255,0.018),0_22px_42px_-22px_rgba(0,0,0,0.9)]\"\n      : \"border-[#eadfce] bg-[#fffcf6] text-[#171717]/76 shadow-[inset_0_1px_0_rgba(88,72,49,0.04),inset_0_12px_24px_rgba(88,72,49,0.022),0_22px_44px_-26px_rgba(66,52,33,0.22)]\",\n    menuItem: isDark ? \"hover:bg-[#232323]\" : \"hover:bg-[#f8f2e7]\",\n    menuActive: isDark ? \"bg-[#232323]\" : \"bg-[#f8f2e7]\",\n    divider: isDark ? \"border-[#232323]\" : \"border-[#f1e8dc]\",\n    primaryText: isDark ? \"text-[#ededed]/90\" : \"text-[#171717]/86\",\n    secondaryText: isDark ? \"text-[#ededed]/38\" : \"text-[#171717]/60\",\n    mutedSurface: isDark ? \"bg-[#181818]\" : \"bg-[#f8f2e7]\",\n    popover: isDark ? \"bg-[#111] text-[#ededed]\" : \"bg-[#fffcf6] text-[#171717]\",\n    accent: isDark ? \"text-[#d86a2c]\" : \"text-[#bd4514]\",\n    accentBg: isDark ? \"bg-[#ec5c13]/12\" : \"bg-[#ec5c13]/14\",\n    accentBorder: isDark ? \"border-[#ec5c13]/24\" : \"border-[#bd4514]/22\",\n    checkboxAccent: isDark ? \"#d86a2c\" : \"#bd4514\",\n  };\n}\n\nexport function getBjorkSignalPalette(\n  tone: \"green\" | \"red\" | \"orange\" | \"neutral\",\n  isDark: boolean\n) {\n  const palettes = {\n    green: {\n      bgColor: isDark ? \"bg-[#3e6f52]/18\" : \"bg-[#dce8dc]\",\n      borderColor: isDark ? \"border-[#5f9f78]/28\" : \"border-[#bad0bd]\",\n      textColor: isDark ? \"text-[#77b58b]\" : \"text-[#47785c]\",\n      dotColor: isDark ? \"bg-[#77b58b]\" : \"bg-[#47785c]\",\n      color: isDark ? \"#77b58b\" : \"#47785c\",\n    },\n    red: {\n      bgColor: isDark ? \"bg-[#87463f]/18\" : \"bg-[#eadad5]\",\n      borderColor: isDark ? \"border-[#c46a61]/28\" : \"border-[#d7bcb4]\",\n      textColor: isDark ? \"text-[#cf7c72]\" : \"text-[#9d5149]\",\n      dotColor: isDark ? \"bg-[#cf7c72]\" : \"bg-[#9d5149]\",\n      color: isDark ? \"#cf7c72\" : \"#9d5149\",\n    },\n    orange: {\n      bgColor: isDark ? \"bg-[#ec5c13]/14\" : \"bg-[#ead9cb]\",\n      borderColor: isDark ? \"border-[#ec5c13]/28\" : \"border-[#d5b59f]\",\n      textColor: isDark ? \"text-[#d86a2c]\" : \"text-[#9f4317]\",\n      dotColor: isDark ? \"bg-[#d86a2c]\" : \"bg-[#9f4317]\",\n      color: isDark ? \"#d86a2c\" : \"#9f4317\",\n    },\n    neutral: {\n      bgColor: isDark ? \"bg-[#ededed]/8\" : \"bg-[#ded7ca]\",\n      borderColor: isDark ? \"border-[#ededed]/14\" : \"border-[#cfc6b7]\",\n      textColor: isDark ? \"text-[#ededed]/58\" : \"text-[#171717]/60\",\n      dotColor: isDark ? \"bg-[#ededed]/45\" : \"bg-[#171717]/42\",\n      color: isDark ? \"#a3a3a3\" : \"#706b62\",\n    },\n  } as const;\n\n  return palettes[tone];\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/tables/table-theme.ts"
    }
  ],
  "cssVars": {
    "light": {
      "bjork-bg": "#f7f3ea",
      "bjork-surface": "#fffcf6",
      "bjork-surface-muted": "rgba(250, 246, 237, 0.94)",
      "bjork-surface-hover": "#f5efe3",
      "bjork-surface-active": "#efe7d8",
      "bjork-panel": "#f8f2e7",
      "bjork-menu": "rgba(255, 252, 246, 0.98)",
      "bjork-field": "rgba(255, 252, 246, 0.96)",
      "bjork-field-muted": "rgba(248, 242, 231, 0.82)",
      "bjork-field-inset": "rgba(239, 231, 216, 0.72)",
      "bjork-error-bg": "#fff0e8",
      "bjork-text": "#171717",
      "bjork-text-strong": "rgba(23, 23, 23, 0.9)",
      "bjork-text-medium": "rgba(23, 23, 23, 0.78)",
      "bjork-text-muted": "rgba(23, 23, 23, 0.68)",
      "bjork-text-soft": "rgba(23, 23, 23, 0.62)",
      "bjork-text-faint": "rgba(23, 23, 23, 0.22)",
      "bjork-inverted-text": "#fff8f0",
      "bjork-border": "#eee6db",
      "bjork-border-muted": "#f5ede2",
      "bjork-border-strong": "#e1d7c8",
      "bjork-thumb": "#a49b8e",
      "bjork-track": "#ded6ca",
      "bjork-accent": "#ec7d43",
      "bjork-accent-hover": "#f0935f",
      "bjork-accent-soft": "rgba(236, 125, 67, 0.1)",
      "bjork-accent-muted": "rgba(236, 125, 67, 0.2)",
      "bjork-accent-badge": "rgba(236, 125, 67, 0.16)",
      "bjork-accent-badge-foreground": "#8d421d",
      "bjork-accent-foreground": "#3f2112",
      "bjork-accent-fill": "#ec7d43",
      "bjork-accent-fill-hover": "#f0935f",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)",
      "bjork-ring-offset": "#f7f3ea"
    },
    "dark": {
      "bjork-bg": "#111111",
      "bjork-surface": "#121212",
      "bjork-surface-muted": "rgba(22, 22, 22, 0.92)",
      "bjork-surface-hover": "#161616",
      "bjork-surface-active": "#202020",
      "bjork-panel": "#0d0d0d",
      "bjork-menu": "rgba(18, 18, 18, 0.98)",
      "bjork-field": "rgba(18, 18, 18, 0.95)",
      "bjork-field-muted": "rgba(18, 18, 18, 0.55)",
      "bjork-field-inset": "rgba(9, 9, 9, 0.75)",
      "bjork-error-bg": "#130d0a",
      "bjork-text": "#ededed",
      "bjork-text-strong": "rgba(237, 237, 237, 0.9)",
      "bjork-text-medium": "rgba(237, 237, 237, 0.72)",
      "bjork-text-muted": "rgba(237, 237, 237, 0.6)",
      "bjork-text-soft": "rgba(237, 237, 237, 0.5)",
      "bjork-text-faint": "rgba(237, 237, 237, 0.22)",
      "bjork-inverted-text": "#080808",
      "bjork-border": "#232323",
      "bjork-border-muted": "#1c1c1c",
      "bjork-border-strong": "#343434",
      "bjork-thumb": "#626262",
      "bjork-track": "#090909",
      "bjork-accent": "#ec5c13",
      "bjork-accent-hover": "#f07832",
      "bjork-accent-soft": "rgba(236, 92, 19, 0.12)",
      "bjork-accent-muted": "rgba(236, 92, 19, 0.24)",
      "bjork-accent-badge": "#b84a12",
      "bjork-accent-badge-foreground": "#fff2ea",
      "bjork-accent-foreground": "#fff2ea",
      "bjork-accent-fill": "#b84a12",
      "bjork-accent-fill-hover": "#a84a14",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(255, 255, 255, 0.045)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(0, 0, 0, 0.45)",
      "bjork-ring-offset": "#080808"
    }
  },
  "css": {
    ".bjork-range": {
      "box-shadow": "inset 0 1px 1px rgba(255, 255, 255, 0.04), 0 0 14px rgba(189, 69, 20, 0.06)"
    },
    ".bjork-range::-webkit-slider-thumb": {
      "appearance": "none",
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-range::-moz-range-thumb": {
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-layered-button-accent, .bjork-layered-button-raised": {
      "position": "relative",
      "isolation": "isolate",
      "appearance": "none",
      "overflow": "hidden",
      "border": "0",
      "border-radius": "13px",
      "box-shadow": "inset 0 1px 0 #ffffff38, 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a",
      "transition": "filter 150ms ease-out, box-shadow 150ms ease-out"
    },
    ".bjork-layered-button-accent::after, .bjork-layered-button-raised::after": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none",
      "content": "\"\"",
      "border": "1.5px solid transparent",
      "border-radius": "inherit",
      "background": "linear-gradient(#ffffffb8, #0000003d 41% 75%, #ffffff47) border-box",
      "mix-blend-mode": "overlay",
      "-webkit-mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "-webkit-mask-clip": "padding-box, border-box",
      "mask-clip": "padding-box, border-box",
      "-webkit-mask-origin": "padding-box, border-box",
      "mask-origin": "padding-box, border-box",
      "-webkit-mask-composite": "xor",
      "mask-composite": "exclude"
    },
    ".bjork-layered-button-accent": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000040",
      "background": "radial-gradient(ellipse at -20px top, rgba(255, 255, 255, 0.14), transparent), linear-gradient(180deg, #ec5c13 0%, #ec5c13 62%, color-mix(in oklab, #ec5c13 84%, black 16%) 100%)"
    },
    ".bjork-layered-button-raised": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, oklch(0.1881 0.006 265), oklch(0.5493 0.0081 265))"
    },
    ".dark .bjork-layered-button-raised": {
      "color": "oklch(0.2574 0.0056 265)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, color-mix(in oklab, oklch(0.9674 0.0013 265) 80%, oklch(0.2574 0.0056 265) 20%), color-mix(in oklab, oklch(0.669 0.0107 265) 80%, oklch(0.2574 0.0056 265) 20%))"
    },
    "@media (hover: hover)": {
      ".bjork-layered-button-accent:hover": {
        "filter": "brightness(1.08) saturate(1.04)"
      },
      ".bjork-layered-button-raised:hover": {
        "filter": "brightness(1.08) contrast(0.9)"
      }
    }
  },
  "meta": {
    "collection": "Tables",
    "registryUrl": "https://ui.isaiahbjork.com/invoice-table.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/invoice-table.json"
  },
  "type": "registry:component"
}