{
  "name": "KHMYS",
  "formatVersion": 1,
  "files": [
    {
      "name": "tokens.css",
      "type": "text/css",
      "content": ":root {\n  --background: 0 0% 100%;\n  --foreground: 224 71% 4%;\n  --card: 0 0% 100%;\n  --card-foreground: 224 71% 4%;\n  --popover: 0 0% 100%;\n  --popover-foreground: 224 71% 4%;\n  --primary: 224 100% 57%;\n  --primary-foreground: 0 0% 100%;\n  --secondary: 220 14% 96%;\n  --secondary-foreground: 224 71% 4%;\n  --muted: 220 14% 96%;\n  --muted-foreground: 220 9% 46%;\n  --accent: 220 14% 96%;\n  --accent-foreground: 224 71% 4%;\n  --destructive: 0 84% 60%;\n  --destructive-foreground: 0 0% 100%;\n  --border: 220 13% 91%;\n  --input: 220 13% 91%;\n  --ring: 224 100% 57%;\n  --radius: 0.5rem;\n  --chart-green: 142 71% 45%;\n  --chart-red: 0 84% 60%;\n  --chart-blue: 224 100% 57%;\n  --chart-purple: 262 83% 58%;\n  --chart-orange: 27 96% 61%;\n  --chart-yellow: 48 96% 53%;\n  --series-income: 142 71% 45%;\n  --series-expense: 0 84% 60%;\n  --series-magnitude: 224 100% 57%;\n  --sidebar-background: 0 0% 98%;\n  --sidebar-foreground: 224 71% 4%;\n  --sidebar-primary: 224 100% 57%;\n  --sidebar-primary-foreground: 0 0% 100%;\n  --sidebar-accent: 220 14% 96%;\n  --sidebar-accent-foreground: 224 71% 4%;\n  --sidebar-border: 220 13% 91%;\n  --sidebar-ring: 224 100% 57%;\n}\n\n.dark {\n  --background: 220 20% 7%;\n  --foreground: 210 20% 98%;\n  --card: 220 18% 10%;\n  --card-foreground: 210 20% 98%;\n  --popover: 220 18% 10%;\n  --popover-foreground: 210 20% 98%;\n  --secondary: 220 16% 14%;\n  --secondary-foreground: 210 20% 98%;\n  --muted: 220 16% 14%;\n  --muted-foreground: 218 11% 55%;\n  --accent: 220 16% 14%;\n  --accent-foreground: 210 20% 98%;\n  --destructive: 0 63% 31%;\n  --border: 220 14% 14%;\n  --input: 220 14% 16%;\n  --series-income: 142 71% 40%;\n  --series-expense: 0 84% 51%;\n  --series-magnitude: 224 100% 64%;\n  --sidebar-background: 220 20% 7%;\n  --sidebar-foreground: 210 20% 90%;\n  --sidebar-accent: 220 16% 13%;\n  --sidebar-accent-foreground: 210 20% 98%;\n  --sidebar-border: 220 14% 12%;\n}\n\n.ui-v2 {\n  --background: 40 20% 98%;\n  --foreground: 20 10% 10%;\n  --card-foreground: 20 10% 10%;\n  --popover-foreground: 20 10% 10%;\n  --primary: 174 60% 40%;\n  --secondary: 40 10% 94%;\n  --secondary-foreground: 20 10% 10%;\n  --muted: 40 10% 96%;\n  --muted-foreground: 20 5% 45%;\n  --accent: 40 10% 94%;\n  --accent-foreground: 20 10% 10%;\n  --border: 40 10% 90%;\n  --input: 40 10% 90%;\n  --ring: 174 60% 40%;\n  --sidebar-background: 40 15% 97%;\n  --sidebar-foreground: 20 10% 10%;\n  --sidebar-primary: 174 60% 40%;\n  --sidebar-accent: 40 10% 94%;\n  --sidebar-accent-foreground: 20 10% 10%;\n  --sidebar-border: 40 10% 92%;\n  --sidebar-ring: 174 60% 40%;\n}\n\n.ui-v2.dark {\n  --background: 0 0% 7%;\n  --foreground: 30 10% 93%;\n  --card: 0 0% 10%;\n  --card-foreground: 30 10% 93%;\n  --popover: 0 0% 10%;\n  --popover-foreground: 30 10% 93%;\n  --primary: 174 60% 45%;\n  --primary-foreground: 0 0% 100%;\n  --secondary: 0 0% 14%;\n  --secondary-foreground: 30 10% 93%;\n  --muted: 0 0% 13%;\n  --muted-foreground: 0 0% 55%;\n  --accent: 0 0% 14%;\n  --accent-foreground: 30 10% 93%;\n  --destructive: 0 72% 51%;\n  --destructive-foreground: 0 0% 100%;\n  --border: 0 0% 15%;\n  --input: 0 0% 17%;\n  --ring: 174 60% 45%;\n  --radius: 0.5rem;\n  --chart-green: 142 60% 50%;\n  --chart-red: 0 75% 60%;\n  --chart-blue: 224 100% 57%;\n  --chart-purple: 262 83% 58%;\n  --chart-orange: 27 96% 61%;\n  --chart-yellow: 48 96% 53%;\n  --series-income: 142 71% 40%;\n  --series-expense: 0 84% 51%;\n  --series-magnitude: 224 100% 64%;\n  --sidebar-background: 0 0% 6%;\n  --sidebar-foreground: 30 10% 85%;\n  --sidebar-primary: 174 60% 45%;\n  --sidebar-primary-foreground: 0 0% 100%;\n  --sidebar-accent: 0 0% 12%;\n  --sidebar-accent-foreground: 30 10% 93%;\n  --sidebar-border: 0 0% 13%;\n  --sidebar-ring: 174 60% 45%;\n}\n\n.landing-dark {\n  --l-bg: 0 0% 4%;\n  --l-fg: 0 0% 98%;\n  --l-muted: 0 0% 62%;\n  --l-line: 0 0% 15%;\n  --l-card: 0 0% 8%;\n  --l-card2: 0 0% 11%;\n  --l-accent: 168 42% 62%;\n  --l-accent-fg: 0 0% 7%;\n  --l-invert: 0 0% 100%;\n  --l-invert-fg: 0 0% 5%;\n  --l-paper: 45 25% 95%;\n  --l-ink: 0 0% 6%;\n  --l-ink-muted: 0 0% 38%;\n}\n\n.landing-light {\n  --l-bg: 45 25% 96%;\n  --l-fg: 0 0% 6%;\n  --l-muted: 0 0% 38%;\n  --l-line: 40 10% 86%;\n  --l-card: 0 0% 100%;\n  --l-card2: 45 20% 93%;\n  --l-accent: 172 62% 30%;\n  --l-accent-fg: 0 0% 100%;\n  --l-invert: 0 0% 6%;\n  --l-invert-fg: 0 0% 100%;\n  --l-paper: 0 0% 5%;\n  --l-ink: 0 0% 98%;\n  --l-ink-muted: 0 0% 62%;\n}\n"
    },
    {
      "name": "tokens.json",
      "type": "application/json",
      "content": "{\n  \"light\": {\n    \"--background\": \"0 0% 100%\",\n    \"--foreground\": \"224 71% 4%\",\n    \"--card\": \"0 0% 100%\",\n    \"--card-foreground\": \"224 71% 4%\",\n    \"--popover\": \"0 0% 100%\",\n    \"--popover-foreground\": \"224 71% 4%\",\n    \"--primary\": \"224 100% 57%\",\n    \"--primary-foreground\": \"0 0% 100%\",\n    \"--secondary\": \"220 14% 96%\",\n    \"--secondary-foreground\": \"224 71% 4%\",\n    \"--muted\": \"220 14% 96%\",\n    \"--muted-foreground\": \"220 9% 46%\",\n    \"--accent\": \"220 14% 96%\",\n    \"--accent-foreground\": \"224 71% 4%\",\n    \"--destructive\": \"0 84% 60%\",\n    \"--destructive-foreground\": \"0 0% 100%\",\n    \"--border\": \"220 13% 91%\",\n    \"--input\": \"220 13% 91%\",\n    \"--ring\": \"224 100% 57%\",\n    \"--radius\": \"0.5rem\",\n    \"--chart-green\": \"142 71% 45%\",\n    \"--chart-red\": \"0 84% 60%\",\n    \"--chart-blue\": \"224 100% 57%\",\n    \"--chart-purple\": \"262 83% 58%\",\n    \"--chart-orange\": \"27 96% 61%\",\n    \"--chart-yellow\": \"48 96% 53%\",\n    \"--series-income\": \"142 71% 45%\",\n    \"--series-expense\": \"0 84% 60%\",\n    \"--series-magnitude\": \"224 100% 57%\",\n    \"--sidebar-background\": \"0 0% 98%\",\n    \"--sidebar-foreground\": \"224 71% 4%\",\n    \"--sidebar-primary\": \"224 100% 57%\",\n    \"--sidebar-primary-foreground\": \"0 0% 100%\",\n    \"--sidebar-accent\": \"220 14% 96%\",\n    \"--sidebar-accent-foreground\": \"224 71% 4%\",\n    \"--sidebar-border\": \"220 13% 91%\",\n    \"--sidebar-ring\": \"224 100% 57%\"\n  },\n  \"dark\": {\n    \"--background\": \"220 20% 7%\",\n    \"--foreground\": \"210 20% 98%\",\n    \"--card\": \"220 18% 10%\",\n    \"--card-foreground\": \"210 20% 98%\",\n    \"--popover\": \"220 18% 10%\",\n    \"--popover-foreground\": \"210 20% 98%\",\n    \"--primary\": \"224 100% 57%\",\n    \"--primary-foreground\": \"0 0% 100%\",\n    \"--secondary\": \"220 16% 14%\",\n    \"--secondary-foreground\": \"210 20% 98%\",\n    \"--muted\": \"220 16% 14%\",\n    \"--muted-foreground\": \"218 11% 55%\",\n    \"--accent\": \"220 16% 14%\",\n    \"--accent-foreground\": \"210 20% 98%\",\n    \"--destructive\": \"0 63% 31%\",\n    \"--destructive-foreground\": \"0 0% 100%\",\n    \"--border\": \"220 14% 14%\",\n    \"--input\": \"220 14% 16%\",\n    \"--ring\": \"224 100% 57%\",\n    \"--radius\": \"0.5rem\",\n    \"--chart-green\": \"142 71% 45%\",\n    \"--chart-red\": \"0 84% 60%\",\n    \"--chart-blue\": \"224 100% 57%\",\n    \"--chart-purple\": \"262 83% 58%\",\n    \"--chart-orange\": \"27 96% 61%\",\n    \"--chart-yellow\": \"48 96% 53%\",\n    \"--series-income\": \"142 71% 40%\",\n    \"--series-expense\": \"0 84% 51%\",\n    \"--series-magnitude\": \"224 100% 64%\",\n    \"--sidebar-background\": \"220 20% 7%\",\n    \"--sidebar-foreground\": \"210 20% 90%\",\n    \"--sidebar-primary\": \"224 100% 57%\",\n    \"--sidebar-primary-foreground\": \"0 0% 100%\",\n    \"--sidebar-accent\": \"220 16% 13%\",\n    \"--sidebar-accent-foreground\": \"210 20% 98%\",\n    \"--sidebar-border\": \"220 14% 12%\",\n    \"--sidebar-ring\": \"224 100% 57%\"\n  },\n  \"alternate\": {\n    \"light\": {\n      \"--background\": \"40 20% 98%\",\n      \"--foreground\": \"20 10% 10%\",\n      \"--card\": \"0 0% 100%\",\n      \"--card-foreground\": \"20 10% 10%\",\n      \"--popover\": \"0 0% 100%\",\n      \"--popover-foreground\": \"20 10% 10%\",\n      \"--primary\": \"174 60% 40%\",\n      \"--primary-foreground\": \"0 0% 100%\",\n      \"--secondary\": \"40 10% 94%\",\n      \"--secondary-foreground\": \"20 10% 10%\",\n      \"--muted\": \"40 10% 96%\",\n      \"--muted-foreground\": \"20 5% 45%\",\n      \"--accent\": \"40 10% 94%\",\n      \"--accent-foreground\": \"20 10% 10%\",\n      \"--destructive\": \"0 84% 60%\",\n      \"--destructive-foreground\": \"0 0% 100%\",\n      \"--border\": \"40 10% 90%\",\n      \"--input\": \"40 10% 90%\",\n      \"--ring\": \"174 60% 40%\",\n      \"--radius\": \"0.5rem\",\n      \"--chart-green\": \"142 71% 45%\",\n      \"--chart-red\": \"0 84% 60%\",\n      \"--chart-blue\": \"224 100% 57%\",\n      \"--chart-purple\": \"262 83% 58%\",\n      \"--chart-orange\": \"27 96% 61%\",\n      \"--chart-yellow\": \"48 96% 53%\",\n      \"--series-income\": \"142 71% 45%\",\n      \"--series-expense\": \"0 84% 60%\",\n      \"--series-magnitude\": \"224 100% 57%\",\n      \"--sidebar-background\": \"40 15% 97%\",\n      \"--sidebar-foreground\": \"20 10% 10%\",\n      \"--sidebar-primary\": \"174 60% 40%\",\n      \"--sidebar-primary-foreground\": \"0 0% 100%\",\n      \"--sidebar-accent\": \"40 10% 94%\",\n      \"--sidebar-accent-foreground\": \"20 10% 10%\",\n      \"--sidebar-border\": \"40 10% 92%\",\n      \"--sidebar-ring\": \"174 60% 40%\"\n    },\n    \"dark\": {\n      \"--background\": \"0 0% 7%\",\n      \"--foreground\": \"30 10% 93%\",\n      \"--card\": \"0 0% 10%\",\n      \"--card-foreground\": \"30 10% 93%\",\n      \"--popover\": \"0 0% 10%\",\n      \"--popover-foreground\": \"30 10% 93%\",\n      \"--primary\": \"174 60% 45%\",\n      \"--primary-foreground\": \"0 0% 100%\",\n      \"--secondary\": \"0 0% 14%\",\n      \"--secondary-foreground\": \"30 10% 93%\",\n      \"--muted\": \"0 0% 13%\",\n      \"--muted-foreground\": \"0 0% 55%\",\n      \"--accent\": \"0 0% 14%\",\n      \"--accent-foreground\": \"30 10% 93%\",\n      \"--destructive\": \"0 72% 51%\",\n      \"--destructive-foreground\": \"0 0% 100%\",\n      \"--border\": \"0 0% 15%\",\n      \"--input\": \"0 0% 17%\",\n      \"--ring\": \"174 60% 45%\",\n      \"--radius\": \"0.5rem\",\n      \"--chart-green\": \"142 60% 50%\",\n      \"--chart-red\": \"0 75% 60%\",\n      \"--chart-blue\": \"224 100% 57%\",\n      \"--chart-purple\": \"262 83% 58%\",\n      \"--chart-orange\": \"27 96% 61%\",\n      \"--chart-yellow\": \"48 96% 53%\",\n      \"--series-income\": \"142 71% 40%\",\n      \"--series-expense\": \"0 84% 51%\",\n      \"--series-magnitude\": \"224 100% 64%\",\n      \"--sidebar-background\": \"0 0% 6%\",\n      \"--sidebar-foreground\": \"30 10% 85%\",\n      \"--sidebar-primary\": \"174 60% 45%\",\n      \"--sidebar-primary-foreground\": \"0 0% 100%\",\n      \"--sidebar-accent\": \"0 0% 12%\",\n      \"--sidebar-accent-foreground\": \"30 10% 93%\",\n      \"--sidebar-border\": \"0 0% 13%\",\n      \"--sidebar-ring\": \"174 60% 45%\"\n    }\n  },\n  \"landing\": {\n    \"dark\": {\n      \"--l-bg\": \"0 0% 4%\",\n      \"--l-fg\": \"0 0% 98%\",\n      \"--l-muted\": \"0 0% 62%\",\n      \"--l-line\": \"0 0% 15%\",\n      \"--l-card\": \"0 0% 8%\",\n      \"--l-card2\": \"0 0% 11%\",\n      \"--l-accent\": \"168 42% 62%\",\n      \"--l-accent-fg\": \"0 0% 7%\",\n      \"--l-invert\": \"0 0% 100%\",\n      \"--l-invert-fg\": \"0 0% 5%\",\n      \"--l-paper\": \"45 25% 95%\",\n      \"--l-ink\": \"0 0% 6%\",\n      \"--l-ink-muted\": \"0 0% 38%\"\n    },\n    \"light\": {\n      \"--l-bg\": \"45 25% 96%\",\n      \"--l-fg\": \"0 0% 6%\",\n      \"--l-muted\": \"0 0% 38%\",\n      \"--l-line\": \"40 10% 86%\",\n      \"--l-card\": \"0 0% 100%\",\n      \"--l-card2\": \"45 20% 93%\",\n      \"--l-accent\": \"172 62% 30%\",\n      \"--l-accent-fg\": \"0 0% 100%\",\n      \"--l-invert\": \"0 0% 6%\",\n      \"--l-invert-fg\": \"0 0% 100%\",\n      \"--l-paper\": \"0 0% 5%\",\n      \"--l-ink\": \"0 0% 98%\",\n      \"--l-ink-muted\": \"0 0% 62%\"\n    }\n  }\n}\n"
    },
    {
      "name": "index.css",
      "type": "text/css",
      "content": "/* Geist Sans, self-hosted (no external font request). Only the weights the\n   app actually uses (font-normal/medium/semibold/bold); latin subset only. */\n@import \"@fontsource/geist-sans/latin-400.css\";\n@import \"@fontsource/geist-sans/latin-500.css\";\n@import \"@fontsource/geist-sans/latin-600.css\";\n@import \"@fontsource/geist-sans/latin-700.css\";\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 224 71% 4%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 224 71% 4%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 224 71% 4%;\n\n    --primary: 224 100% 57%;\n    --primary-foreground: 0 0% 100%;\n\n    --secondary: 220 14% 96%;\n    --secondary-foreground: 224 71% 4%;\n\n    --muted: 220 14% 96%;\n    --muted-foreground: 220 9% 46%;\n\n    --accent: 220 14% 96%;\n    --accent-foreground: 224 71% 4%;\n\n    --destructive: 0 84% 60%;\n    --destructive-foreground: 0 0% 100%;\n\n    --border: 220 13% 91%;\n    --input: 220 13% 91%;\n    --ring: 224 100% 57%;\n\n    --radius: 0.5rem;\n\n    --chart-green: 142 71% 45%;\n    --chart-red: 0 84% 60%;\n    --chart-blue: 224 100% 57%;\n    --chart-purple: 262 83% 58%;\n    --chart-orange: 27 96% 61%;\n    --chart-yellow: 48 96% 53%;\n\n    /* Chart series tokens — validated with the data-viz palette checker against\n       the light card surface (#fff): lightness band, chroma floor, CVD\n       separation and contrast all pass. The income/expense pair sits in the\n       6-8 CVD floor band, which is legal only alongside secondary encoding, so\n       those charts always ship a legend plus direct value labels. */\n    --series-income: 142 71% 45%;\n    --series-expense: 0 84% 60%;\n    /* Single-hue ramp for magnitude (spend-by-category bars). */\n    --series-magnitude: 224 100% 57%;\n\n    --sidebar-background: 0 0% 98%;\n    --sidebar-foreground: 224 71% 4%;\n    --sidebar-primary: 224 100% 57%;\n    --sidebar-primary-foreground: 0 0% 100%;\n    --sidebar-accent: 220 14% 96%;\n    --sidebar-accent-foreground: 224 71% 4%;\n    --sidebar-border: 220 13% 91%;\n    --sidebar-ring: 224 100% 57%;\n  }\n\n  .dark {\n    --background: 220 20% 7%;\n    --foreground: 210 20% 98%;\n\n    --card: 220 18% 10%;\n    --card-foreground: 210 20% 98%;\n\n    --popover: 220 18% 10%;\n    --popover-foreground: 210 20% 98%;\n\n    --primary: 224 100% 57%;\n    --primary-foreground: 0 0% 100%;\n\n    --secondary: 220 16% 14%;\n    --secondary-foreground: 210 20% 98%;\n\n    --muted: 220 16% 14%;\n    --muted-foreground: 218 11% 55%;\n\n    --accent: 220 16% 14%;\n    --accent-foreground: 210 20% 98%;\n\n    --destructive: 0 63% 31%;\n    --destructive-foreground: 0 0% 100%;\n\n    --border: 220 14% 14%;\n    --input: 220 14% 16%;\n    --ring: 224 100% 57%;\n\n    --chart-green: 142 71% 45%;\n    --chart-red: 0 84% 60%;\n    --chart-blue: 224 100% 57%;\n    --chart-purple: 262 83% 58%;\n    --chart-orange: 27 96% 61%;\n    --chart-yellow: 48 96% 53%;\n\n    /* Dark steps are re-derived for the dark card surface, not the light values\n       reused — the previous tokens were identical in both themes. */\n    --series-income: 142 71% 40%;\n    --series-expense: 0 84% 51%;\n    --series-magnitude: 224 100% 64%;\n\n    --sidebar-background: 220 20% 7%;\n    --sidebar-foreground: 210 20% 90%;\n    --sidebar-primary: 224 100% 57%;\n    --sidebar-primary-foreground: 0 0% 100%;\n    --sidebar-accent: 220 16% 13%;\n    --sidebar-accent-foreground: 210 20% 98%;\n    --sidebar-border: 220 14% 12%;\n    --sidebar-ring: 224 100% 57%;\n  }\n}\n\n/* ─── V2 Perplexity-Inspired Theme ─── */\n@layer base {\n  .ui-v2 {\n    --background: 40 20% 98%;\n    --foreground: 20 10% 10%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 20 10% 10%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 20 10% 10%;\n\n    --primary: 174 60% 40%;\n    --primary-foreground: 0 0% 100%;\n\n    --secondary: 40 10% 94%;\n    --secondary-foreground: 20 10% 10%;\n\n    --muted: 40 10% 96%;\n    --muted-foreground: 20 5% 45%;\n\n    --accent: 40 10% 94%;\n    --accent-foreground: 20 10% 10%;\n\n    --destructive: 0 84% 60%;\n    --destructive-foreground: 0 0% 100%;\n\n    --border: 40 10% 90%;\n    --input: 40 10% 90%;\n    --ring: 174 60% 40%;\n\n    --sidebar-background: 40 15% 97%;\n    --sidebar-foreground: 20 10% 10%;\n    --sidebar-primary: 174 60% 40%;\n    --sidebar-primary-foreground: 0 0% 100%;\n    --sidebar-accent: 40 10% 94%;\n    --sidebar-accent-foreground: 20 10% 10%;\n    --sidebar-border: 40 10% 92%;\n    --sidebar-ring: 174 60% 40%;\n  }\n\n  .ui-v2.dark {\n    --background: 0 0% 7%;\n    --foreground: 30 10% 93%;\n\n    --card: 0 0% 10%;\n    --card-foreground: 30 10% 93%;\n\n    --popover: 0 0% 10%;\n    --popover-foreground: 30 10% 93%;\n\n    --primary: 174 60% 45%;\n    --primary-foreground: 0 0% 100%;\n\n    --secondary: 0 0% 14%;\n    --secondary-foreground: 30 10% 93%;\n\n    --muted: 0 0% 13%;\n    --muted-foreground: 0 0% 55%;\n\n    --accent: 0 0% 14%;\n    --accent-foreground: 30 10% 93%;\n\n    --destructive: 0 72% 51%;\n    --destructive-foreground: 0 0% 100%;\n\n    --border: 0 0% 15%;\n    --input: 0 0% 17%;\n    --ring: 174 60% 45%;\n\n    --chart-green: 142 60% 50%;\n    --chart-red: 0 75% 60%;\n\n    --sidebar-background: 0 0% 6%;\n    --sidebar-foreground: 30 10% 85%;\n    --sidebar-primary: 174 60% 45%;\n    --sidebar-primary-foreground: 0 0% 100%;\n    --sidebar-accent: 0 0% 12%;\n    --sidebar-accent-foreground: 30 10% 93%;\n    --sidebar-border: 0 0% 13%;\n    --sidebar-ring: 174 60% 45%;\n  }\n}\n\n/* ─── V2 Typography & Component Overrides ─── */\n.ui-v2 body {\n  font-size: 14px;\n  line-height: 1.5;\n  letter-spacing: -0.011em;\n}\n\n.ui-v2 h1, .ui-v2 h2, .ui-v2 h3, .ui-v2 h4, .ui-v2 h5, .ui-v2 h6 {\n  font-weight: 600;\n  letter-spacing: -0.03em;\n}\n\n.ui-v2 .shadow-sm {\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);\n}\n\n/* Added briefly around a theme change (see lib/themeTransition.ts) so the whole\n   page flips in one frame instead of fading element by element. */\n.theme-switching,\n.theme-switching *,\n.theme-switching *::before,\n.theme-switching *::after {\n  transition: none !important;\n}\n\n.ui-v2 * {\n  transition-property: color, background-color, border-color;\n  transition-duration: 150ms;\n  transition-timing-function: ease;\n}\n\n/* ─── V2 Button Overrides ───\n   Scoped to real <button> elements only. This used to also match any\n   [role=\"button\"] div (a clickable dropzone, a budget row you click to edit),\n   which pressed and scaled those down as if they were buttons, and felt\n   wrong on things that don't look like one. */\n.ui-v2 button {\n  border-radius: 0.5rem;\n  transition: background-color 300ms ease-out, border-color 300ms ease-out, color 300ms ease-out, opacity 300ms ease-out;\n}\n\n/* V2 primary buttons: inverted fg/bg like Perplexity */\n.ui-v2 .bg-primary {\n  background-color: hsl(var(--foreground)) !important;\n  color: hsl(var(--background)) !important;\n}\n.ui-v2 .bg-primary:hover {\n  background-color: hsl(var(--foreground) / 0.85) !important;\n}\n\n/* V2 outline/default buttons: subtle border style */\n.ui-v2 .border-input {\n  border-color: hsl(var(--border));\n}\n.ui-v2 .border-input:hover {\n  background-color: hsl(var(--muted));\n  border-color: hsl(var(--muted));\n}\n\n/* ─── V2 Sidebar Icon Uniformity ─── */\n.ui-v2 [data-sidebar=\"menu\"] svg,\n.ui-v2 [data-sidebar=\"content\"] svg {\n  color: currentColor;\n  opacity: 0.7;\n}\n\n.ui-v2 [data-sidebar=\"menu\"] a[data-active=\"true\"] svg,\n.ui-v2 [data-sidebar=\"content\"] a.bg-sidebar-accent svg {\n  opacity: 1;\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n\n  body {\n    @apply bg-background text-foreground;\n    font-family: 'Geist Sans', system-ui, -apple-system, sans-serif;\n    letter-spacing: -0.01em;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n  }\n\n  h1, h2, h3, h4, h5, h6 {\n    letter-spacing: -0.025em;\n  }\n}\n\n@keyframes fade-in-up {\n  from {\n    opacity: 0;\n    transform: translateY(12px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n/* Route change: a quick fade, no movement (the old slide-up made every page\n   and settings tab look like it was rising from the bottom). */\n@keyframes page-in {\n  from { opacity: 0; }\n  to { opacity: 1; }\n}\n\n.animate-page-in {\n  animation: page-in 0.18s ease-out both;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .animate-page-in { animation: none; }\n}\n\n.animate-fade-in-up {\n  animation: fade-in-up 0.4s ease-out both;\n}\n\n/* Public landing page: scroll reveal (src/lib/reveal.ts), hero rise (used by\n   the static pre-render, which can't run the intro animation without delaying\n   first paint) and a cross-fade between full page loads. */\n.reveal-pending { opacity: 0; transform: translateY(28px); }\n.reveal-run {\n  transition: opacity 0.8s cubic-bezier(0.22, 0.61, 0.36, 1), transform 0.8s cubic-bezier(0.22, 0.61, 0.36, 1);\n}\n@keyframes hero-rise {\n  from { opacity: 0.35; transform: translateY(16px); }\n  to { opacity: 1; transform: none; }\n}\n.hero-rise { animation: hero-rise 0.8s cubic-bezier(0.22, 0.61, 0.36, 1) both; }\n.hero-rise-2 { animation-delay: 0.12s; }\n@view-transition { navigation: auto; }\n::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.28s; }\n@media (prefers-reduced-motion: reduce) {\n  .reveal-pending { opacity: 1; transform: none; }\n  .hero-rise { animation: none; }\n  @view-transition { navigation: none; }\n}\n\n/* Hide scrollbar utility */\n.scrollbar-hide {\n  -ms-overflow-style: none;\n  scrollbar-width: none;\n}\n.scrollbar-hide::-webkit-scrollbar {\n  display: none;\n}\n\n/* Sidebar label fade transitions */\n[data-state=\"collapsed\"] [data-sidebar=\"sidebar\"] .sidebar-label {\n  opacity: 0;\n  max-width: 0;\n  overflow: hidden;\n  pointer-events: none;\n  transform: translateX(-4px);\n  transition: opacity 200ms ease-out, max-width 200ms ease-out, transform 200ms ease-out;\n}\n[data-state=\"expanded\"] [data-sidebar=\"sidebar\"] .sidebar-label {\n  opacity: 1;\n  max-width: 200px;\n  overflow: hidden;\n  transform: translateX(0);\n  transition: opacity 250ms 80ms ease-out, max-width 250ms 80ms ease-out, transform 250ms 80ms ease-out;\n}\n\n/* Reports' Print button: the sidebar, its rail and the sticky header are all\n   navigation chrome with nothing to print. Layout's <main> already drops its\n   own padding and scroll clipping via print:p-0/print:overflow-visible; this\n   is the rest, global because the sidebar mounts outside any page's markup. */\n@media print {\n  /* [data-sidebar=\"sidebar\"] is only the visible panel; its parent (marked\n     by data-side + data-variant, unique to Sidebar's own root) is what\n     actually reserves the layout gap on desktop and has to go too, or the\n     page prints with the sidebar's width left blank on the left. */\n  [data-side][data-variant],\n  [data-sidebar=\"sidebar\"],\n  [data-sidebar=\"rail\"] {\n    display: none !important;\n  }\n}\n\n/* The logo follows each surface's theme, independently of the app root. */\n.brand-wordmark {\n  color: hsl(172 62% 30%);\n}\n.dark .brand-wordmark {\n  color: hsl(168 42% 62%);\n}\n.landing-light .brand-wordmark {\n  color: hsl(var(--l-accent));\n}\n.landing-dark .brand-wordmark {\n  color: hsl(var(--l-accent));\n}\n\n/* ── Public landing page palette ─────────────────────────────────────────── */\n.landing-dark {\n  --l-bg: 0 0% 4%;\n  --l-fg: 0 0% 98%;\n  --l-muted: 0 0% 62%;\n  --l-line: 0 0% 15%;\n  --l-card: 0 0% 8%;\n  --l-card2: 0 0% 11%;\n  --l-accent: 168 42% 62%;\n  --l-accent-fg: 0 0% 7%;\n  --l-invert: 0 0% 100%;\n  --l-invert-fg: 0 0% 5%;\n  --l-paper: 45 25% 95%;\n  --l-ink: 0 0% 6%;\n  --l-ink-muted: 0 0% 38%;\n}\n.landing-light {\n  --l-bg: 45 25% 96%;\n  --l-fg: 0 0% 6%;\n  --l-muted: 0 0% 38%;\n  --l-line: 40 10% 86%;\n  --l-card: 0 0% 100%;\n  --l-card2: 45 20% 93%;\n  --l-accent: 172 62% 30%;\n  --l-accent-fg: 0 0% 100%;\n  --l-invert: 0 0% 6%;\n  --l-invert-fg: 0 0% 100%;\n  --l-paper: 0 0% 5%;\n  --l-ink: 0 0% 98%;\n  --l-ink-muted: 0 0% 62%;\n}\n"
    },
    {
      "name": "tailwind.config.ts",
      "type": "text/plain",
      "content": "import type { Config } from \"tailwindcss\";\nimport animate from \"tailwindcss-animate\";\n\nexport default {\n  darkMode: [\"class\"],\n  content: [\"./pages/**/*.{ts,tsx}\", \"./components/**/*.{ts,tsx}\", \"./app/**/*.{ts,tsx}\", \"./src/**/*.{ts,tsx}\"],\n  prefix: \"\",\n  theme: {\n    fontFamily: {\n      sans: [\"Geist Sans\", \"system-ui\", \"-apple-system\", \"sans-serif\"],\n    },\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      colors: {\n        // Public landing page palette (see .landing-dark / .landing-light in index.css).\n        land: {\n          bg: \"hsl(var(--l-bg) / <alpha-value>)\",\n          fg: \"hsl(var(--l-fg) / <alpha-value>)\",\n          muted: \"hsl(var(--l-muted) / <alpha-value>)\",\n          line: \"hsl(var(--l-line) / <alpha-value>)\",\n          card: \"hsl(var(--l-card) / <alpha-value>)\",\n          card2: \"hsl(var(--l-card2) / <alpha-value>)\",\n          accent: \"hsl(var(--l-accent) / <alpha-value>)\",\n          \"accent-fg\": \"hsl(var(--l-accent-fg) / <alpha-value>)\",\n          invert: \"hsl(var(--l-invert) / <alpha-value>)\",\n          \"invert-fg\": \"hsl(var(--l-invert-fg) / <alpha-value>)\",\n          paper: \"hsl(var(--l-paper) / <alpha-value>)\",\n          ink: \"hsl(var(--l-ink) / <alpha-value>)\",\n          \"ink-muted\": \"hsl(var(--l-ink-muted) / <alpha-value>)\",\n        },\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n        sidebar: {\n          DEFAULT: \"hsl(var(--sidebar-background))\",\n          foreground: \"hsl(var(--sidebar-foreground))\",\n          primary: \"hsl(var(--sidebar-primary))\",\n          \"primary-foreground\": \"hsl(var(--sidebar-primary-foreground))\",\n          accent: \"hsl(var(--sidebar-accent))\",\n          \"accent-foreground\": \"hsl(var(--sidebar-accent-foreground))\",\n          border: \"hsl(var(--sidebar-border))\",\n          ring: \"hsl(var(--sidebar-ring))\",\n        },\n        series: {\n          income: \"hsl(var(--series-income))\",\n          expense: \"hsl(var(--series-expense))\",\n          magnitude: \"hsl(var(--series-magnitude))\",\n        },\n        chart: {\n          green: \"hsl(var(--chart-green))\",\n          red: \"hsl(var(--chart-red))\",\n          blue: \"hsl(var(--chart-blue))\",\n          purple: \"hsl(var(--chart-purple))\",\n          orange: \"hsl(var(--chart-orange))\",\n          yellow: \"hsl(var(--chart-yellow))\",\n        },\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: \"0\" },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: \"0\" },\n        },\n        \"slide-up\": {\n          from: { opacity: \"0\", transform: \"translateY(100%)\" },\n          to: { opacity: \"1\", transform: \"translateY(0)\" },\n        },\n        \"slide-down-out\": {\n          from: { opacity: \"1\", transform: \"translateY(0)\" },\n          to: { opacity: \"0\", transform: \"translateY(100%)\" },\n        },\n        \"fade-in\": {\n          from: { opacity: \"0\", transform: \"translateY(8px)\" },\n          to: { opacity: \"1\", transform: \"translateY(0)\" },\n        },\n        \"scale-in\": {\n          from: { opacity: \"0\", transform: \"scale(0.95)\" },\n          to: { opacity: \"1\", transform: \"scale(1)\" },\n        },\n        \"collapsible-down\": {\n          from: { height: \"0\" },\n          to: { height: \"var(--radix-collapsible-content-height)\" },\n        },\n        \"collapsible-up\": {\n          from: { height: \"var(--radix-collapsible-content-height)\" },\n          to: { height: \"0\" },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n        \"collapsible-down\": \"collapsible-down 0.2s ease-out\",\n        \"collapsible-up\": \"collapsible-up 0.2s ease-out\",\n        \"slide-up\": \"slide-up 0.3s cubic-bezier(0.16, 1, 0.3, 1)\",\n        \"slide-down-out\": \"slide-down-out 0.2s ease-in forwards\",\n        \"fade-in\": \"fade-in 0.3s ease-out both\",\n        \"scale-in\": \"scale-in 0.2s ease-out both\",\n      },\n    },\n  },\n  plugins: [animate],\n} satisfies Config;\n"
    },
    {
      "name": "Logo.tsx",
      "type": "text/plain",
      "content": "import { cn } from \"@/lib/utils\";\n\n/**\n * Architype Stedelijk: the five khmy$ glyph outlines from The Foundry's\n * specimen, set at their original advance widths with no added tracking.\n * Source: https://www.thefoundrytypes.com/fonts/architype-stedelijk/\n * Only the fixed logo artwork is included; no font software is bundled.\n */\nconst WORDMARK_PATH = [\n  \"M17 667 Q10 667 5 662 Q0 657 0 650 L0 17 Q0 10 5 5 Q10 0 17 0 L78 0 Q85 0 90 5 Q95 10 95 17 L95 364 Q95 371 100 376 Q105 381 112 381 L269 381 Q276 381 281 376 Q286 371 286 364 L286 208 Q286 201 291 196 Q296 191 303 191 L364 191 Q371 191 376 196 Q381 201 381 208 L381 364 Q381 371 376 376 Q371 381 364 381 L303 381 Q296 381 291 386 Q286 391 286 398 L286 460 Q286 467 291 472 Q296 477 303 477 L364 477 Q371 477 376 482 Q381 487 381 494 L381 650 Q381 657 376 662 Q371 667 364 667 L303 667 Q296 667 291 662 Q286 657 286 650 L286 494 Q286 487 281 482 Q276 477 269 477 L112 477 Q105 477 100 482 Q95 487 95 494 L95 650 Q95 657 90 662 Q85 667 78 667 L17 667 Z\",\n  \"M422 667 Q415 667 410 662 Q405 657 405 650 L405 17 Q405 10 410 5 Q415 0 422 0 L483 0 Q490 0 495 5 Q500 10 500 17 L500 174 Q500 181 505 186 Q510 191 517 191 L674 191 Q681 191 686 196 Q691 201 691 208 L691 269 Q691 276 696 281 Q701 286 708 286 L769 286 Q776 286 781 291 Q786 296 786 303 L786 650 Q786 657 781 662 Q776 667 769 667 L708 667 Q701 667 696 662 Q691 657 691 650 L691 303 Q691 296 686 291 Q681 286 674 286 L517 286 Q510 286 505 291 Q500 296 500 303 L500 650 Q500 657 495 662 Q490 667 483 667 L422 667 Z\",\n  \"M827 667 Q820 667 815 662 Q810 657 810 650 L810 208 Q810 201 815 196 Q820 191 827 191 L1079 191 Q1086 191 1091 196 Q1096 201 1096 208 L1096 269 Q1096 276 1101 281 Q1106 286 1113 286 L1174 286 Q1181 286 1186 281 Q1191 276 1191 269 L1191 208 Q1191 201 1196 196 Q1201 191 1208 191 L1365 191 Q1372 191 1377 196 Q1382 201 1382 208 L1382 269 Q1382 276 1387 281 Q1392 286 1399 286 L1460 286 Q1467 286 1472 291 Q1477 296 1477 303 L1477 650 Q1477 657 1472 662 Q1467 667 1460 667 L1399 667 Q1392 667 1387 662 Q1382 657 1382 650 L1382 303 Q1382 296 1377 291 Q1372 286 1365 286 L1208 286 Q1201 286 1196 291 Q1191 296 1191 303 L1191 650 Q1191 657 1186 662 Q1181 667 1174 667 L1113 667 Q1106 667 1101 662 Q1096 657 1096 650 L1096 303 Q1096 296 1091 291 Q1086 286 1079 286 L922 286 Q915 286 910 291 Q905 296 905 303 L905 650 Q905 657 900 662 Q895 667 888 667 L827 667 Z\",\n  \"M1613 667 Q1606 667 1601 662 Q1596 657 1596 650 L1596 589 Q1596 582 1591 577 Q1586 572 1579 572 L1518 572 Q1511 572 1506 567 Q1501 562 1501 555 L1501 208 Q1501 201 1506 196 Q1511 191 1518 191 L1579 191 Q1586 191 1591 196 Q1596 201 1596 208 L1596 555 Q1596 562 1601 567 Q1606 572 1613 572 L1770 572 Q1777 572 1782 567 Q1787 562 1787 555 L1787 208 Q1787 201 1792 196 Q1797 191 1804 191 L1865 191 Q1872 191 1877 196 Q1882 201 1882 208 L1882 745 Q1882 752 1877 757 Q1872 762 1865 762 L1804 762 Q1797 762 1792 767 Q1787 772 1787 779 L1787 840 Q1787 847 1782 852 Q1777 857 1770 857 L1518 857 Q1511 857 1506 852 Q1501 847 1501 840 L1501 779 Q1501 772 1506 767 Q1511 762 1518 762 L1770 762 Q1777 762 1782 757 Q1787 752 1787 745 L1787 684 Q1787 677 1782 672 Q1777 667 1770 667 L1613 667 Z\",\n  \"M1923 667 Q1916 667 1911 662 Q1906 657 1906 650 L1906 589 Q1906 582 1911 577 Q1916 572 1923 572 L2175 572 Q2182 572 2187 567 Q2192 562 2192 555 L2192 494 Q2192 487 2187 482 Q2182 477 2175 477 L1923 477 Q1916 477 1911 472 Q1906 467 1906 460 L1906 303 Q1906 296 1911 291 Q1916 286 1923 286 L1984 286 Q1991 286 1996 281 Q2001 276 2001 269 L2001 208 Q2001 201 2006 196 Q2011 191 2018 191 L2079 191 Q2086 191 2091 186 Q2096 181 2096 174 L2096 113 Q2096 106 2101 101 Q2106 96 2113 96 L2175 96 Q2182 96 2187 101 Q2192 106 2192 113 L2192 174 Q2192 181 2197 186 Q2202 191 2209 191 L2270 191 Q2277 191 2282 196 Q2287 201 2287 208 L2287 269 Q2287 276 2282 281 Q2277 286 2270 286 L2018 286 Q2011 286 2006 291 Q2001 296 2001 303 L2001 364 Q2001 371 2006 376 Q2011 381 2018 381 L2270 381 Q2277 381 2282 386 Q2287 391 2287 398 L2287 555 Q2287 562 2282 567 Q2277 572 2270 572 L2209 572 Q2202 572 2197 577 Q2192 582 2192 589 L2192 745 Q2192 752 2187 757 Q2182 762 2175 762 L2113 762 Q2106 762 2101 757 Q2096 752 2096 745 L2096 684 Q2096 677 2091 672 Q2086 667 2079 667 L1923 667 Z\",\n].join(\" \");\n\nconst MONOGRAM_PATH = \"M17 667 Q10 667 5 662 Q0 657 0 650 L0 17 Q0 10 5 5 Q10 0 17 0 L78 0 Q85 0 90 5 Q95 10 95 17 L95 364 Q95 371 100 376 Q105 381 112 381 L269 381 Q276 381 281 376 Q286 371 286 364 L286 208 Q286 201 291 196 Q296 191 303 191 L364 191 Q371 191 376 196 Q381 201 381 208 L381 364 Q381 371 376 376 Q371 381 364 381 L303 381 Q296 381 291 386 Q286 391 286 398 L286 460 Q286 467 291 472 Q296 477 303 477 L364 477 Q371 477 376 482 Q381 487 381 494 L381 650 Q381 657 376 662 Q371 667 364 667 L303 667 Q296 667 291 662 Q286 657 286 650 L286 494 Q286 487 281 482 Q276 477 269 477 L112 477 Q105 477 100 482 Q95 487 95 494 L95 650 Q95 657 90 662 Q85 667 78 667 L17 667 Z\";\n\nexport type LogoTone = \"landing\" | \"app\" | \"mono\" | \"inverse\" | \"onPrimary\" | \"teal\";\n\nconst TONES: Record<LogoTone, string> = {\n  landing: \"brand-wordmark\",\n  app: \"brand-wordmark\",\n  mono: \"text-foreground\",\n  inverse: \"text-background\",\n  onPrimary: \"text-primary-foreground\",\n  teal: \"text-[hsl(174_60%_40%)]\",\n};\n\n/** Architype Stedelijk khmy$. Size by height to preserve the letterforms. */\nexport function Logo({ className, tone = \"landing\" }: { className?: string; tone?: LogoTone }) {\n  return (\n    <svg viewBox=\"0 0 2287 857\" role=\"img\" aria-label=\"KHMYS\" focusable=\"false\" className={cn(\"h-7 w-auto shrink-0\", TONES[tone], className)} fill=\"currentColor\">\n      <path d={WORDMARK_PATH} />\n    </svg>\n  );\n}\n\n/** The same Architype Stedelijk k for square browser icons and avatars. */\nexport function LogoMark({ className, tone = \"landing\" }: { className?: string; tone?: LogoTone }) {\n  return (\n    <svg viewBox=\"-238 -95 857 857\" role=\"img\" aria-label=\"KHMYS\" focusable=\"false\" className={cn(\"h-7 w-7 shrink-0\", TONES[tone], className)} fill=\"currentColor\">\n      <path d={MONOGRAM_PATH} />\n    </svg>\n  );\n}\n\n/** Standalone outlined wordmark for brand-kit downloads. */\nexport function logoSvg(color: string): string {\n  if (!/^#[0-9a-f]{6}$/i.test(color)) throw new Error(\"Logo export requires a six-digit hex color\");\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2287 857\" role=\"img\" aria-label=\"KHMYS\"><path fill=\"${color}\" d=\"${WORDMARK_PATH}\"/></svg>\\n`;\n}\n\n/** Browser icon using the same Stedelijk k, white against a dark surface. */\nexport function faviconSvg(): string {\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"-238 -95 857 857\"><rect x=\"-238\" y=\"-95\" width=\"857\" height=\"857\" rx=\"170\" fill=\"#12312C\"/><path fill=\"#FFFFFF\" d=\"${MONOGRAM_PATH}\"/></svg>\\n`;\n}\n"
    },
    {
      "name": "khmys-mark-app.svg",
      "type": "image/svg+xml",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2287 857\" role=\"img\" aria-label=\"KHMYS\"><path fill=\"#1D7C6F\" d=\"M17 667 Q10 667 5 662 Q0 657 0 650 L0 17 Q0 10 5 5 Q10 0 17 0 L78 0 Q85 0 90 5 Q95 10 95 17 L95 364 Q95 371 100 376 Q105 381 112 381 L269 381 Q276 381 281 376 Q286 371 286 364 L286 208 Q286 201 291 196 Q296 191 303 191 L364 191 Q371 191 376 196 Q381 201 381 208 L381 364 Q381 371 376 376 Q371 381 364 381 L303 381 Q296 381 291 386 Q286 391 286 398 L286 460 Q286 467 291 472 Q296 477 303 477 L364 477 Q371 477 376 482 Q381 487 381 494 L381 650 Q381 657 376 662 Q371 667 364 667 L303 667 Q296 667 291 662 Q286 657 286 650 L286 494 Q286 487 281 482 Q276 477 269 477 L112 477 Q105 477 100 482 Q95 487 95 494 L95 650 Q95 657 90 662 Q85 667 78 667 L17 667 Z M422 667 Q415 667 410 662 Q405 657 405 650 L405 17 Q405 10 410 5 Q415 0 422 0 L483 0 Q490 0 495 5 Q500 10 500 17 L500 174 Q500 181 505 186 Q510 191 517 191 L674 191 Q681 191 686 196 Q691 201 691 208 L691 269 Q691 276 696 281 Q701 286 708 286 L769 286 Q776 286 781 291 Q786 296 786 303 L786 650 Q786 657 781 662 Q776 667 769 667 L708 667 Q701 667 696 662 Q691 657 691 650 L691 303 Q691 296 686 291 Q681 286 674 286 L517 286 Q510 286 505 291 Q500 296 500 303 L500 650 Q500 657 495 662 Q490 667 483 667 L422 667 Z M827 667 Q820 667 815 662 Q810 657 810 650 L810 208 Q810 201 815 196 Q820 191 827 191 L1079 191 Q1086 191 1091 196 Q1096 201 1096 208 L1096 269 Q1096 276 1101 281 Q1106 286 1113 286 L1174 286 Q1181 286 1186 281 Q1191 276 1191 269 L1191 208 Q1191 201 1196 196 Q1201 191 1208 191 L1365 191 Q1372 191 1377 196 Q1382 201 1382 208 L1382 269 Q1382 276 1387 281 Q1392 286 1399 286 L1460 286 Q1467 286 1472 291 Q1477 296 1477 303 L1477 650 Q1477 657 1472 662 Q1467 667 1460 667 L1399 667 Q1392 667 1387 662 Q1382 657 1382 650 L1382 303 Q1382 296 1377 291 Q1372 286 1365 286 L1208 286 Q1201 286 1196 291 Q1191 296 1191 303 L1191 650 Q1191 657 1186 662 Q1181 667 1174 667 L1113 667 Q1106 667 1101 662 Q1096 657 1096 650 L1096 303 Q1096 296 1091 291 Q1086 286 1079 286 L922 286 Q915 286 910 291 Q905 296 905 303 L905 650 Q905 657 900 662 Q895 667 888 667 L827 667 Z M1613 667 Q1606 667 1601 662 Q1596 657 1596 650 L1596 589 Q1596 582 1591 577 Q1586 572 1579 572 L1518 572 Q1511 572 1506 567 Q1501 562 1501 555 L1501 208 Q1501 201 1506 196 Q1511 191 1518 191 L1579 191 Q1586 191 1591 196 Q1596 201 1596 208 L1596 555 Q1596 562 1601 567 Q1606 572 1613 572 L1770 572 Q1777 572 1782 567 Q1787 562 1787 555 L1787 208 Q1787 201 1792 196 Q1797 191 1804 191 L1865 191 Q1872 191 1877 196 Q1882 201 1882 208 L1882 745 Q1882 752 1877 757 Q1872 762 1865 762 L1804 762 Q1797 762 1792 767 Q1787 772 1787 779 L1787 840 Q1787 847 1782 852 Q1777 857 1770 857 L1518 857 Q1511 857 1506 852 Q1501 847 1501 840 L1501 779 Q1501 772 1506 767 Q1511 762 1518 762 L1770 762 Q1777 762 1782 757 Q1787 752 1787 745 L1787 684 Q1787 677 1782 672 Q1777 667 1770 667 L1613 667 Z M1923 667 Q1916 667 1911 662 Q1906 657 1906 650 L1906 589 Q1906 582 1911 577 Q1916 572 1923 572 L2175 572 Q2182 572 2187 567 Q2192 562 2192 555 L2192 494 Q2192 487 2187 482 Q2182 477 2175 477 L1923 477 Q1916 477 1911 472 Q1906 467 1906 460 L1906 303 Q1906 296 1911 291 Q1916 286 1923 286 L1984 286 Q1991 286 1996 281 Q2001 276 2001 269 L2001 208 Q2001 201 2006 196 Q2011 191 2018 191 L2079 191 Q2086 191 2091 186 Q2096 181 2096 174 L2096 113 Q2096 106 2101 101 Q2106 96 2113 96 L2175 96 Q2182 96 2187 101 Q2192 106 2192 113 L2192 174 Q2192 181 2197 186 Q2202 191 2209 191 L2270 191 Q2277 191 2282 196 Q2287 201 2287 208 L2287 269 Q2287 276 2282 281 Q2277 286 2270 286 L2018 286 Q2011 286 2006 291 Q2001 296 2001 303 L2001 364 Q2001 371 2006 376 Q2011 381 2018 381 L2270 381 Q2277 381 2282 386 Q2287 391 2287 398 L2287 555 Q2287 562 2282 567 Q2277 572 2270 572 L2209 572 Q2202 572 2197 577 Q2192 582 2192 589 L2192 745 Q2192 752 2187 757 Q2182 762 2175 762 L2113 762 Q2106 762 2101 757 Q2096 752 2096 745 L2096 684 Q2096 677 2091 672 Q2086 667 2079 667 L1923 667 Z\"/></svg>\n"
    },
    {
      "name": "khmys-mark-public-dark.svg",
      "type": "image/svg+xml",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2287 857\" role=\"img\" aria-label=\"KHMYS\"><path fill=\"#FFFFFF\" d=\"M17 667 Q10 667 5 662 Q0 657 0 650 L0 17 Q0 10 5 5 Q10 0 17 0 L78 0 Q85 0 90 5 Q95 10 95 17 L95 364 Q95 371 100 376 Q105 381 112 381 L269 381 Q276 381 281 376 Q286 371 286 364 L286 208 Q286 201 291 196 Q296 191 303 191 L364 191 Q371 191 376 196 Q381 201 381 208 L381 364 Q381 371 376 376 Q371 381 364 381 L303 381 Q296 381 291 386 Q286 391 286 398 L286 460 Q286 467 291 472 Q296 477 303 477 L364 477 Q371 477 376 482 Q381 487 381 494 L381 650 Q381 657 376 662 Q371 667 364 667 L303 667 Q296 667 291 662 Q286 657 286 650 L286 494 Q286 487 281 482 Q276 477 269 477 L112 477 Q105 477 100 482 Q95 487 95 494 L95 650 Q95 657 90 662 Q85 667 78 667 L17 667 Z M422 667 Q415 667 410 662 Q405 657 405 650 L405 17 Q405 10 410 5 Q415 0 422 0 L483 0 Q490 0 495 5 Q500 10 500 17 L500 174 Q500 181 505 186 Q510 191 517 191 L674 191 Q681 191 686 196 Q691 201 691 208 L691 269 Q691 276 696 281 Q701 286 708 286 L769 286 Q776 286 781 291 Q786 296 786 303 L786 650 Q786 657 781 662 Q776 667 769 667 L708 667 Q701 667 696 662 Q691 657 691 650 L691 303 Q691 296 686 291 Q681 286 674 286 L517 286 Q510 286 505 291 Q500 296 500 303 L500 650 Q500 657 495 662 Q490 667 483 667 L422 667 Z M827 667 Q820 667 815 662 Q810 657 810 650 L810 208 Q810 201 815 196 Q820 191 827 191 L1079 191 Q1086 191 1091 196 Q1096 201 1096 208 L1096 269 Q1096 276 1101 281 Q1106 286 1113 286 L1174 286 Q1181 286 1186 281 Q1191 276 1191 269 L1191 208 Q1191 201 1196 196 Q1201 191 1208 191 L1365 191 Q1372 191 1377 196 Q1382 201 1382 208 L1382 269 Q1382 276 1387 281 Q1392 286 1399 286 L1460 286 Q1467 286 1472 291 Q1477 296 1477 303 L1477 650 Q1477 657 1472 662 Q1467 667 1460 667 L1399 667 Q1392 667 1387 662 Q1382 657 1382 650 L1382 303 Q1382 296 1377 291 Q1372 286 1365 286 L1208 286 Q1201 286 1196 291 Q1191 296 1191 303 L1191 650 Q1191 657 1186 662 Q1181 667 1174 667 L1113 667 Q1106 667 1101 662 Q1096 657 1096 650 L1096 303 Q1096 296 1091 291 Q1086 286 1079 286 L922 286 Q915 286 910 291 Q905 296 905 303 L905 650 Q905 657 900 662 Q895 667 888 667 L827 667 Z M1613 667 Q1606 667 1601 662 Q1596 657 1596 650 L1596 589 Q1596 582 1591 577 Q1586 572 1579 572 L1518 572 Q1511 572 1506 567 Q1501 562 1501 555 L1501 208 Q1501 201 1506 196 Q1511 191 1518 191 L1579 191 Q1586 191 1591 196 Q1596 201 1596 208 L1596 555 Q1596 562 1601 567 Q1606 572 1613 572 L1770 572 Q1777 572 1782 567 Q1787 562 1787 555 L1787 208 Q1787 201 1792 196 Q1797 191 1804 191 L1865 191 Q1872 191 1877 196 Q1882 201 1882 208 L1882 745 Q1882 752 1877 757 Q1872 762 1865 762 L1804 762 Q1797 762 1792 767 Q1787 772 1787 779 L1787 840 Q1787 847 1782 852 Q1777 857 1770 857 L1518 857 Q1511 857 1506 852 Q1501 847 1501 840 L1501 779 Q1501 772 1506 767 Q1511 762 1518 762 L1770 762 Q1777 762 1782 757 Q1787 752 1787 745 L1787 684 Q1787 677 1782 672 Q1777 667 1770 667 L1613 667 Z M1923 667 Q1916 667 1911 662 Q1906 657 1906 650 L1906 589 Q1906 582 1911 577 Q1916 572 1923 572 L2175 572 Q2182 572 2187 567 Q2192 562 2192 555 L2192 494 Q2192 487 2187 482 Q2182 477 2175 477 L1923 477 Q1916 477 1911 472 Q1906 467 1906 460 L1906 303 Q1906 296 1911 291 Q1916 286 1923 286 L1984 286 Q1991 286 1996 281 Q2001 276 2001 269 L2001 208 Q2001 201 2006 196 Q2011 191 2018 191 L2079 191 Q2086 191 2091 186 Q2096 181 2096 174 L2096 113 Q2096 106 2101 101 Q2106 96 2113 96 L2175 96 Q2182 96 2187 101 Q2192 106 2192 113 L2192 174 Q2192 181 2197 186 Q2202 191 2209 191 L2270 191 Q2277 191 2282 196 Q2287 201 2287 208 L2287 269 Q2287 276 2282 281 Q2277 286 2270 286 L2018 286 Q2011 286 2006 291 Q2001 296 2001 303 L2001 364 Q2001 371 2006 376 Q2011 381 2018 381 L2270 381 Q2277 381 2282 386 Q2287 391 2287 398 L2287 555 Q2287 562 2282 567 Q2277 572 2270 572 L2209 572 Q2202 572 2197 577 Q2192 582 2192 589 L2192 745 Q2192 752 2187 757 Q2182 762 2175 762 L2113 762 Q2106 762 2101 757 Q2096 752 2096 745 L2096 684 Q2096 677 2091 672 Q2086 667 2079 667 L1923 667 Z\"/></svg>\n"
    },
    {
      "name": "khmys-mark-public-light.svg",
      "type": "image/svg+xml",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2287 857\" role=\"img\" aria-label=\"KHMYS\"><path fill=\"#1D7C6F\" d=\"M17 667 Q10 667 5 662 Q0 657 0 650 L0 17 Q0 10 5 5 Q10 0 17 0 L78 0 Q85 0 90 5 Q95 10 95 17 L95 364 Q95 371 100 376 Q105 381 112 381 L269 381 Q276 381 281 376 Q286 371 286 364 L286 208 Q286 201 291 196 Q296 191 303 191 L364 191 Q371 191 376 196 Q381 201 381 208 L381 364 Q381 371 376 376 Q371 381 364 381 L303 381 Q296 381 291 386 Q286 391 286 398 L286 460 Q286 467 291 472 Q296 477 303 477 L364 477 Q371 477 376 482 Q381 487 381 494 L381 650 Q381 657 376 662 Q371 667 364 667 L303 667 Q296 667 291 662 Q286 657 286 650 L286 494 Q286 487 281 482 Q276 477 269 477 L112 477 Q105 477 100 482 Q95 487 95 494 L95 650 Q95 657 90 662 Q85 667 78 667 L17 667 Z M422 667 Q415 667 410 662 Q405 657 405 650 L405 17 Q405 10 410 5 Q415 0 422 0 L483 0 Q490 0 495 5 Q500 10 500 17 L500 174 Q500 181 505 186 Q510 191 517 191 L674 191 Q681 191 686 196 Q691 201 691 208 L691 269 Q691 276 696 281 Q701 286 708 286 L769 286 Q776 286 781 291 Q786 296 786 303 L786 650 Q786 657 781 662 Q776 667 769 667 L708 667 Q701 667 696 662 Q691 657 691 650 L691 303 Q691 296 686 291 Q681 286 674 286 L517 286 Q510 286 505 291 Q500 296 500 303 L500 650 Q500 657 495 662 Q490 667 483 667 L422 667 Z M827 667 Q820 667 815 662 Q810 657 810 650 L810 208 Q810 201 815 196 Q820 191 827 191 L1079 191 Q1086 191 1091 196 Q1096 201 1096 208 L1096 269 Q1096 276 1101 281 Q1106 286 1113 286 L1174 286 Q1181 286 1186 281 Q1191 276 1191 269 L1191 208 Q1191 201 1196 196 Q1201 191 1208 191 L1365 191 Q1372 191 1377 196 Q1382 201 1382 208 L1382 269 Q1382 276 1387 281 Q1392 286 1399 286 L1460 286 Q1467 286 1472 291 Q1477 296 1477 303 L1477 650 Q1477 657 1472 662 Q1467 667 1460 667 L1399 667 Q1392 667 1387 662 Q1382 657 1382 650 L1382 303 Q1382 296 1377 291 Q1372 286 1365 286 L1208 286 Q1201 286 1196 291 Q1191 296 1191 303 L1191 650 Q1191 657 1186 662 Q1181 667 1174 667 L1113 667 Q1106 667 1101 662 Q1096 657 1096 650 L1096 303 Q1096 296 1091 291 Q1086 286 1079 286 L922 286 Q915 286 910 291 Q905 296 905 303 L905 650 Q905 657 900 662 Q895 667 888 667 L827 667 Z M1613 667 Q1606 667 1601 662 Q1596 657 1596 650 L1596 589 Q1596 582 1591 577 Q1586 572 1579 572 L1518 572 Q1511 572 1506 567 Q1501 562 1501 555 L1501 208 Q1501 201 1506 196 Q1511 191 1518 191 L1579 191 Q1586 191 1591 196 Q1596 201 1596 208 L1596 555 Q1596 562 1601 567 Q1606 572 1613 572 L1770 572 Q1777 572 1782 567 Q1787 562 1787 555 L1787 208 Q1787 201 1792 196 Q1797 191 1804 191 L1865 191 Q1872 191 1877 196 Q1882 201 1882 208 L1882 745 Q1882 752 1877 757 Q1872 762 1865 762 L1804 762 Q1797 762 1792 767 Q1787 772 1787 779 L1787 840 Q1787 847 1782 852 Q1777 857 1770 857 L1518 857 Q1511 857 1506 852 Q1501 847 1501 840 L1501 779 Q1501 772 1506 767 Q1511 762 1518 762 L1770 762 Q1777 762 1782 757 Q1787 752 1787 745 L1787 684 Q1787 677 1782 672 Q1777 667 1770 667 L1613 667 Z M1923 667 Q1916 667 1911 662 Q1906 657 1906 650 L1906 589 Q1906 582 1911 577 Q1916 572 1923 572 L2175 572 Q2182 572 2187 567 Q2192 562 2192 555 L2192 494 Q2192 487 2187 482 Q2182 477 2175 477 L1923 477 Q1916 477 1911 472 Q1906 467 1906 460 L1906 303 Q1906 296 1911 291 Q1916 286 1923 286 L1984 286 Q1991 286 1996 281 Q2001 276 2001 269 L2001 208 Q2001 201 2006 196 Q2011 191 2018 191 L2079 191 Q2086 191 2091 186 Q2096 181 2096 174 L2096 113 Q2096 106 2101 101 Q2106 96 2113 96 L2175 96 Q2182 96 2187 101 Q2192 106 2192 113 L2192 174 Q2192 181 2197 186 Q2202 191 2209 191 L2270 191 Q2277 191 2282 196 Q2287 201 2287 208 L2287 269 Q2287 276 2282 281 Q2277 286 2270 286 L2018 286 Q2011 286 2006 291 Q2001 296 2001 303 L2001 364 Q2001 371 2006 376 Q2011 381 2018 381 L2270 381 Q2277 381 2282 386 Q2287 391 2287 398 L2287 555 Q2287 562 2282 567 Q2277 572 2270 572 L2209 572 Q2202 572 2197 577 Q2192 582 2192 589 L2192 745 Q2192 752 2187 757 Q2182 762 2175 762 L2113 762 Q2106 762 2101 757 Q2096 752 2096 745 L2096 684 Q2096 677 2091 672 Q2086 667 2079 667 L1923 667 Z\"/></svg>\n"
    },
    {
      "name": "khmys-mark-mono.svg",
      "type": "image/svg+xml",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2287 857\" role=\"img\" aria-label=\"KHMYS\"><path fill=\"#030711\" d=\"M17 667 Q10 667 5 662 Q0 657 0 650 L0 17 Q0 10 5 5 Q10 0 17 0 L78 0 Q85 0 90 5 Q95 10 95 17 L95 364 Q95 371 100 376 Q105 381 112 381 L269 381 Q276 381 281 376 Q286 371 286 364 L286 208 Q286 201 291 196 Q296 191 303 191 L364 191 Q371 191 376 196 Q381 201 381 208 L381 364 Q381 371 376 376 Q371 381 364 381 L303 381 Q296 381 291 386 Q286 391 286 398 L286 460 Q286 467 291 472 Q296 477 303 477 L364 477 Q371 477 376 482 Q381 487 381 494 L381 650 Q381 657 376 662 Q371 667 364 667 L303 667 Q296 667 291 662 Q286 657 286 650 L286 494 Q286 487 281 482 Q276 477 269 477 L112 477 Q105 477 100 482 Q95 487 95 494 L95 650 Q95 657 90 662 Q85 667 78 667 L17 667 Z M422 667 Q415 667 410 662 Q405 657 405 650 L405 17 Q405 10 410 5 Q415 0 422 0 L483 0 Q490 0 495 5 Q500 10 500 17 L500 174 Q500 181 505 186 Q510 191 517 191 L674 191 Q681 191 686 196 Q691 201 691 208 L691 269 Q691 276 696 281 Q701 286 708 286 L769 286 Q776 286 781 291 Q786 296 786 303 L786 650 Q786 657 781 662 Q776 667 769 667 L708 667 Q701 667 696 662 Q691 657 691 650 L691 303 Q691 296 686 291 Q681 286 674 286 L517 286 Q510 286 505 291 Q500 296 500 303 L500 650 Q500 657 495 662 Q490 667 483 667 L422 667 Z M827 667 Q820 667 815 662 Q810 657 810 650 L810 208 Q810 201 815 196 Q820 191 827 191 L1079 191 Q1086 191 1091 196 Q1096 201 1096 208 L1096 269 Q1096 276 1101 281 Q1106 286 1113 286 L1174 286 Q1181 286 1186 281 Q1191 276 1191 269 L1191 208 Q1191 201 1196 196 Q1201 191 1208 191 L1365 191 Q1372 191 1377 196 Q1382 201 1382 208 L1382 269 Q1382 276 1387 281 Q1392 286 1399 286 L1460 286 Q1467 286 1472 291 Q1477 296 1477 303 L1477 650 Q1477 657 1472 662 Q1467 667 1460 667 L1399 667 Q1392 667 1387 662 Q1382 657 1382 650 L1382 303 Q1382 296 1377 291 Q1372 286 1365 286 L1208 286 Q1201 286 1196 291 Q1191 296 1191 303 L1191 650 Q1191 657 1186 662 Q1181 667 1174 667 L1113 667 Q1106 667 1101 662 Q1096 657 1096 650 L1096 303 Q1096 296 1091 291 Q1086 286 1079 286 L922 286 Q915 286 910 291 Q905 296 905 303 L905 650 Q905 657 900 662 Q895 667 888 667 L827 667 Z M1613 667 Q1606 667 1601 662 Q1596 657 1596 650 L1596 589 Q1596 582 1591 577 Q1586 572 1579 572 L1518 572 Q1511 572 1506 567 Q1501 562 1501 555 L1501 208 Q1501 201 1506 196 Q1511 191 1518 191 L1579 191 Q1586 191 1591 196 Q1596 201 1596 208 L1596 555 Q1596 562 1601 567 Q1606 572 1613 572 L1770 572 Q1777 572 1782 567 Q1787 562 1787 555 L1787 208 Q1787 201 1792 196 Q1797 191 1804 191 L1865 191 Q1872 191 1877 196 Q1882 201 1882 208 L1882 745 Q1882 752 1877 757 Q1872 762 1865 762 L1804 762 Q1797 762 1792 767 Q1787 772 1787 779 L1787 840 Q1787 847 1782 852 Q1777 857 1770 857 L1518 857 Q1511 857 1506 852 Q1501 847 1501 840 L1501 779 Q1501 772 1506 767 Q1511 762 1518 762 L1770 762 Q1777 762 1782 757 Q1787 752 1787 745 L1787 684 Q1787 677 1782 672 Q1777 667 1770 667 L1613 667 Z M1923 667 Q1916 667 1911 662 Q1906 657 1906 650 L1906 589 Q1906 582 1911 577 Q1916 572 1923 572 L2175 572 Q2182 572 2187 567 Q2192 562 2192 555 L2192 494 Q2192 487 2187 482 Q2182 477 2175 477 L1923 477 Q1916 477 1911 472 Q1906 467 1906 460 L1906 303 Q1906 296 1911 291 Q1916 286 1923 286 L1984 286 Q1991 286 1996 281 Q2001 276 2001 269 L2001 208 Q2001 201 2006 196 Q2011 191 2018 191 L2079 191 Q2086 191 2091 186 Q2096 181 2096 174 L2096 113 Q2096 106 2101 101 Q2106 96 2113 96 L2175 96 Q2182 96 2187 101 Q2192 106 2192 113 L2192 174 Q2192 181 2197 186 Q2202 191 2209 191 L2270 191 Q2277 191 2282 196 Q2287 201 2287 208 L2287 269 Q2287 276 2282 281 Q2277 286 2270 286 L2018 286 Q2011 286 2006 291 Q2001 296 2001 303 L2001 364 Q2001 371 2006 376 Q2011 381 2018 381 L2270 381 Q2277 381 2282 386 Q2287 391 2287 398 L2287 555 Q2287 562 2282 567 Q2277 572 2270 572 L2209 572 Q2202 572 2197 577 Q2192 582 2192 589 L2192 745 Q2192 752 2187 757 Q2182 762 2175 762 L2113 762 Q2106 762 2101 757 Q2096 752 2096 745 L2096 684 Q2096 677 2091 672 Q2086 667 2079 667 L1923 667 Z\"/></svg>\n"
    },
    {
      "name": "BRAND-GUIDE.md",
      "type": "text/markdown",
      "content": "# KHMYS brand kit\n\nUse the Architype Stedelijk khmy$ wordmark and Geist Sans for interface text.\nThe five letters are outlined as SVG with natural spacing and no added tracking.\nThe wordmark is teal in both modes: a deeper teal on light surfaces and a lighter teal (hsl 168 42% 62%) in dark mode. No font file is bundled.\nReference: https://khmys.com/branding\nMarkdown reference: https://khmys.com/branding/guide.md\nComplete JSON kit: https://khmys.com/branding/kit.json\nIndividual files: https://khmys.com/branding/files/ (append the file's name/path).\n\n## Files\n- tokens.css: all current app and landing theme token blocks.\n- tokens.json: the same values for programmatic use.\n- index.css: complete stylesheet, including V2 overrides and reduced-motion behavior.\n- tailwind.config.ts: the product's actual Tailwind 3 configuration.\n- Logo.tsx: shared React wordmark and compact k (uses @/lib/utils).\n- SVG wordmarks: standalone vectors with fixed colors; choose the correct surface.\n- Complete JSON bundle: includes every production file under src/components,\n  including product widgets, account/investment/transaction dialogs and helpers.\n\n## Source coverage and previews\nThe inventory scans every component subfolder, excludes tests, and offers source\ninspection and individual downloads. Live examples use fictional data and local\ncallbacks. Product components that require authenticated providers are included as\nsource references, not mounted with real accounts. Those files require their app\ncontexts/hooks/API types; this kit does not export credentials or runtime user data.\n\n## Adopting the kit\nApply presentation changes one screen at a time. Preserve behavior, authentication,\nauthorization and existing data flows. Verify light/dark, keyboard focus and mobile.\nInstall the imports your screen uses: React 18, Tailwind 3, tailwindcss-animate,\n@fontsource/geist-sans, lucide-react, Radix UI, class-variance-authority, clsx and\ntailwind-merge. Charts use visx. The source files are references, not a standalone\napplication or an installable component package. Preserve the project's licenses.\n\n## Money and language\nUse shared currency/date formatters and tabular figures. State currency and period.\nNever add unlike currencies without an explicit conversion. Mask amounts in privacy\nmode. Use clear action labels and calm errors. Never display credentials or provider\npayloads. All brand-page examples are fictional.\n\n## Accessibility\nName icon-only controls, keep visible labels, connect errors to fields and check\ncontrast in the selected theme. The page's token contrast report can show failures;\ndo not interpret a preview as a claim that every product screen is WCAG compliant.\n"
    },
    {
      "name": "src/components/AppSidebar.tsx",
      "type": "text/plain",
      "content": "import { DASHBOARD_PATH } from \"@/lib/deployment\";\nimport {\n  LayoutDashboard, BarChart3, FileBarChart, ArrowLeftRight, TrendingUp,\n  Lightbulb, Building2, ChevronRight, ListTodo, ListChecks,\n  Sun, Moon, Database, Link2, Sliders, UserCircle, LogOut, ChevronsUpDown,\n  PanelLeftClose, PanelLeft, Bell, ExternalLink, KeyRound, Wallet, History,\n  type LucideIcon,\n} from \"lucide-react\";\nimport { Link, useLocation, useNavigate } from \"react-router-dom\";\nimport { useMemo } from \"react\";\nimport { NavLink } from \"@/components/NavLink\";\nimport {\n  Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel,\n  SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarFooter, SidebarHeader, useSidebar,\n} from \"@/components/ui/sidebar\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@/components/ui/collapsible\";\nimport {\n  DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,\n  DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent,\n  DropdownMenuSeparator, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuLabel,\n} from \"@/components/ui/dropdown-menu\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { useTheme } from \"@/context/ThemeContext\";\nimport { useAuth } from \"@/context/AuthContext\";\nimport { useNotifications } from \"@/hooks/useNotifications\";\nimport { useActivity } from \"@/hooks/useActivity\";\nimport { usePendingMerchantCategories } from \"@/hooks/useMerchantCategoryReview\";\nimport { useSuggestedTransfers } from \"@/hooks/useTransfers\";\nimport { useSuggestedDuplicates } from \"@/hooks/useDuplicates\";\nimport { activityTitle } from \"@/lib/activity\";\nimport { formatDate } from \"@/lib/format\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport { cn } from \"@/lib/utils\";\nimport { accountDisplayName, accountMask, isBankAccount, isAssetAccount } from \"@/lib/accounts\";\nimport { Logo } from \"@/components/brand/Logo\";\nimport type { Account } from \"@/types/finance\";\n\nconst ICON_SIZE = 18;\nconst ICON_STROKE = 1.5;\n\nconst mainNav = [\n  { title: \"Home\", url: DASHBOARD_PATH, icon: LayoutDashboard },\n  { title: \"Analytics\", url: \"/analytics\", icon: BarChart3 },\n  { title: \"Reports\", url: \"/reports\", icon: FileBarChart },\n  { title: \"Transactions\", url: \"/transactions\", icon: ArrowLeftRight },\n];\n\n/** Children of the Financial Planning parent item. */\nconst planningChildren = [\n  { title: \"Goals Tracker\", url: \"/goals\" },\n  { title: \"Budgets\", url: \"/budgets\" },\n];\n\n// Non-financial \"life\" surfaces, the start of the control-panel vision:\n// Todos today, room for Health/etc. alongside it later.\nconst lifeNav = [\n  { title: \"Todos\", url: \"/todos\", icon: ListTodo },\n];\n\ntype NavItem = { title: string; url: string; icon: LucideIcon; badge?: number };\n\nconst navLinkBase =\n  \"flex items-center gap-3 px-3 py-2 text-sm text-sidebar-foreground/70 transition-colors duration-150 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground rounded-md\";\nconst navLinkActive = \"bg-sidebar-accent text-sidebar-accent-foreground font-medium\";\n\n/** Chevron button; rotation uses Collapsible root `group` + `group-data-[state=open]` on the icon (Radix sets data-state on root). */\nconst collapsibleChevronTriggerClass =\n  \"absolute right-1 top-1/2 z-10 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-sidebar-foreground outline-none ring-sidebar-ring transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 data-[state=open]:bg-sidebar-accent/60\";\n\nconst collapsibleChevronIconClass =\n  \"size-4 shrink-0 transition-transform duration-200 ease-out group-data-[state=open]:rotate-90\";\n\n/** Group accounts under their institution, each group sorted by name. */\nfunction groupByInstitution(list: Account[]) {\n  const m = new Map<string, Account[]>();\n  list.forEach((a) => {\n    const key = (a.institution || \"Other\").trim() || \"Other\";\n    if (!m.has(key)) m.set(key, []);\n    m.get(key)!.push(a);\n  });\n  return Array.from(m.entries())\n    .map(([institution, accs]) => ({\n      institution,\n      accounts: [...accs].sort((x, y) => (accountDisplayName(x) || x.mask || \"\").localeCompare(accountDisplayName(y) || y.mask || \"\")),\n    }))\n    .sort((a, b) => a.institution.localeCompare(b.institution));\n}\n\nexport function AppSidebar() {\n  const { state, toggleSidebar } = useSidebar();\n  const collapsed = state === \"collapsed\";\n  const { investmentAccounts, accounts, dateFormat } = useFinanceData();\n  const notifications = useNotifications();\n  const unreadCount = notifications.filter((n) => n.actionNeeded).length;\n  const { data: recentActivity = [] } = useActivity(4);\n  const { data: pendingCategories = [] } = usePendingMerchantCategories();\n  const { data: suggestedTransfers = [] } = useSuggestedTransfers();\n  const { data: suggestedDuplicates = [] } = useSuggestedDuplicates();\n  const reviewCount = pendingCategories.length + suggestedTransfers.length + suggestedDuplicates.length;\n  const lifeNavItems: NavItem[] = [\n    ...lifeNav,\n    { title: \"Review\", url: \"/review\", icon: ListChecks, badge: reviewCount },\n  ];\n  const { theme, toggleTheme } = useTheme();\n  const { user, logout } = useAuth();\n  const location = useLocation();\n  const navigate = useNavigate();\n  const displayName = user?.name || user?.email?.split(\"@\")[0] || \"Account\";\n  const avatarInitial = (user?.name || user?.email || \"?\").trim().charAt(0).toUpperCase();\n  const isInvestmentsActive = location.pathname.startsWith(\"/investments\");\n  // An account's detail page (/accounts/<id>) belongs to whichever list it is\n  // on: Accounts for bank accounts, Assets for the rest.\n  const detailId = location.pathname.startsWith(\"/accounts/\") ? decodeURIComponent(location.pathname.split(\"/\")[2] ?? \"\") : \"\";\n  const filteredId = location.pathname === \"/transactions\" ? new URLSearchParams(location.search).get(\"account\") : null;\n  const focused = accounts.find((a) => a.id === (detailId || filteredId));\n  const onAsset = !!focused && isAssetAccount(focused.type);\n  const isAccountsActive = location.pathname === \"/accounts\" || (!!focused && !onAsset);\n  const isAssetsActive = location.pathname === \"/assets\" || onAsset;\n\n  const bankTree = useMemo(() => groupByInstitution(accounts.filter((a) => isBankAccount(a.type))), [accounts]);\n  const assetTree = useMemo(() => groupByInstitution(accounts.filter((a) => isAssetAccount(a.type))), [accounts]);\n\n  const renderAccountGroup = (opts: {\n    title: string; url: string; icon: typeof Building2; active: boolean;\n    tree: ReturnType<typeof groupByInstitution>; emptyLabel: string; emptyTo: string;\n  }) => (\n    <SidebarMenuItem>\n      <Collapsible defaultOpen={opts.active} className=\"group w-full min-w-0\">\n        <div className=\"relative w-full\">\n          <SidebarMenuButton asChild isActive={opts.active} className=\"pr-9\">\n            <Link to={opts.url} className=\"gap-2\">\n              <opts.icon size={ICON_SIZE} strokeWidth={ICON_STROKE} className=\"shrink-0\" />\n              <span className=\"sidebar-label truncate\">{opts.title}</span>\n            </Link>\n          </SidebarMenuButton>\n          <CollapsibleTrigger className={collapsibleChevronTriggerClass} aria-label={`Toggle ${opts.title.toLowerCase()} menu`}>\n            <ChevronRight className={collapsibleChevronIconClass} strokeWidth={ICON_STROKE} />\n          </CollapsibleTrigger>\n        </div>\n        <CollapsibleContent className=\"sidebar-label overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\">\n          <div className=\"mt-1 space-y-2 border-l border-sidebar-border/50 pl-2.5 ml-2 py-0.5\">\n            {opts.tree.length === 0 ? (\n              <SidebarMenu className=\"gap-0.5\">\n                <SidebarMenuItem>\n                  <SidebarMenuButton asChild size=\"sm\">\n                    <Link to={opts.emptyTo} className=\"text-muted-foreground\">{opts.emptyLabel}</Link>\n                  </SidebarMenuButton>\n                </SidebarMenuItem>\n              </SidebarMenu>\n            ) : (\n              opts.tree.map(({ institution, accounts: instAccounts }) => (\n                <div key={institution} className=\"min-w-0 space-y-0.5\">\n                  <p className=\"truncate px-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\">\n                    {institution}\n                  </p>\n                  <SidebarMenu className=\"gap-0.5\">\n                    {instAccounts.map((acc) => {\n                      const acctUrl = `/accounts/${encodeURIComponent(acc.id)}`;\n                      const isAcctActive = location.pathname === acctUrl;\n                      const name = accountDisplayName(acc);\n                      const mask = accountMask(acc);\n                      const label = mask ? `${name} ··${mask}` : name;\n                      return (\n                        <SidebarMenuItem key={acc.id}>\n                          <SidebarMenuButton asChild size=\"sm\" isActive={isAcctActive}>\n                            <Link to={acctUrl} className=\"min-w-0\">\n                              <span className=\"w-full truncate text-left\" title={label}>\n                                {name}\n                                {mask ? <span className=\"text-muted-foreground font-normal\"> ··{mask}</span> : null}\n                              </span>\n                            </Link>\n                          </SidebarMenuButton>\n                        </SidebarMenuItem>\n                      );\n                    })}\n                  </SidebarMenu>\n                </div>\n              ))\n            )}\n          </div>\n        </CollapsibleContent>\n      </Collapsible>\n    </SidebarMenuItem>\n  );\n\n  const renderNavItems = (items: NavItem[]) => (\n    <SidebarMenu>\n      {items.map((item) => (\n        <SidebarMenuItem key={item.title}>\n          <SidebarMenuButton asChild>\n            <NavLink\n              to={item.url}\n              end={item.url === DASHBOARD_PATH}\n              className={navLinkBase}\n              activeClassName={navLinkActive}\n            >\n              <item.icon size={ICON_SIZE} strokeWidth={ICON_STROKE} className=\"shrink-0\" />\n              <span className=\"sidebar-label flex-1\">{item.title}</span>\n              {!!item.badge && (\n                <span className=\"sidebar-label ml-auto rounded-full bg-chart-yellow/15 px-1.5 text-[11px] font-medium tabular-nums text-chart-yellow\">\n                  {item.badge}\n                </span>\n              )}\n            </NavLink>\n          </SidebarMenuButton>\n        </SidebarMenuItem>\n      ))}\n    </SidebarMenu>\n  );\n\n  return (\n    <Sidebar collapsible=\"icon\" className=\"border-r border-sidebar-border/50\">\n      {/* Header, brand + collapse toggle aligned with breadcrumbs */}\n      <SidebarHeader className=\"p-0\">\n        <div className={cn(\n          \"flex items-center h-12 px-4 border-b border-sidebar-border/30\",\n          collapsed ? \"justify-center px-2\" : \"justify-between\"\n        )}>\n          <Link\n            to={DASHBOARD_PATH}\n            aria-label=\"KHMYS home\"\n            className=\"text-base font-semibold text-sidebar-foreground tracking-tight sidebar-label whitespace-nowrap rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring\"\n          >\n            <Logo tone=\"app\" className=\"h-7\" />\n          </Link>\n          <button\n            onClick={toggleSidebar}\n            className=\"p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-sidebar-accent transition-colors shrink-0\"\n            aria-label={collapsed ? \"Expand sidebar\" : \"Collapse sidebar\"}\n          >\n            {collapsed ? (\n              <PanelLeft size={16} strokeWidth={ICON_STROKE} />\n            ) : (\n              <PanelLeftClose size={16} strokeWidth={ICON_STROKE} />\n            )}\n          </button>\n        </div>\n      </SidebarHeader>\n\n      <SidebarContent className=\"overflow-y-auto scrollbar-hide\">\n        {/* Main nav */}\n        <SidebarGroup>\n          <SidebarGroupLabel className=\"text-[10px] uppercase tracking-wider text-muted-foreground/60 font-semibold px-3 mb-0.5 sidebar-label\">Overview</SidebarGroupLabel>\n          <SidebarGroupContent>\n            {renderNavItems(mainNav)}\n          </SidebarGroupContent>\n        </SidebarGroup>\n\n        {/* Investments */}\n        <SidebarGroup>\n          <SidebarGroupContent>\n            <SidebarMenu>\n              <SidebarMenuItem>\n                <Collapsible defaultOpen={isInvestmentsActive} className=\"group w-full min-w-0\">\n                  <div className=\"relative w-full\">\n                    <SidebarMenuButton asChild isActive={isInvestmentsActive} className=\"pr-9\">\n                      <Link to=\"/investments\" className=\"gap-2\">\n                        <TrendingUp size={ICON_SIZE} strokeWidth={ICON_STROKE} className=\"shrink-0\" />\n                        <span className=\"sidebar-label truncate\">Investments</span>\n                      </Link>\n                    </SidebarMenuButton>\n                    <CollapsibleTrigger className={collapsibleChevronTriggerClass} aria-label=\"Toggle investments menu\">\n                      <ChevronRight className={collapsibleChevronIconClass} strokeWidth={ICON_STROKE} />\n                    </CollapsibleTrigger>\n                  </div>\n                  <CollapsibleContent className=\"sidebar-label overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\">\n                    <div className=\"mt-1 space-y-0.5 border-l border-sidebar-border/50 pl-2.5 ml-2 py-0.5\">\n                      <SidebarMenu className=\"gap-0.5\">\n                        {investmentAccounts.map((acct) => (\n                          <SidebarMenuItem key={acct.id}>\n                            <SidebarMenuButton asChild size=\"sm\" isActive={location.pathname === `/investments/${acct.id}`}>\n                              <NavLink to={`/investments/${acct.id}`}>{acct.label}</NavLink>\n                            </SidebarMenuButton>\n                          </SidebarMenuItem>\n                        ))}\n                      </SidebarMenu>\n                    </div>\n                  </CollapsibleContent>\n                </Collapsible>\n              </SidebarMenuItem>\n            </SidebarMenu>\n          </SidebarGroupContent>\n        </SidebarGroup>\n\n        {/* Accounts (everyday bank accounts) and Assets (investments, property, vehicles, loans) */}\n        <SidebarGroup>\n          <SidebarGroupContent>\n            <SidebarMenu>\n              {renderAccountGroup({\n                title: \"Accounts\", url: \"/accounts\", icon: Building2, active: isAccountsActive,\n                tree: bankTree, emptyLabel: \"Connect accounts\", emptyTo: \"/settings/sources?connect=1\",\n              })}\n              {renderAccountGroup({\n                title: \"Assets\", url: \"/assets\", icon: Wallet, active: isAssetsActive,\n                tree: assetTree, emptyLabel: \"Add an asset\", emptyTo: \"/assets\",\n              })}\n            </SidebarMenu>\n          </SidebarGroupContent>\n        </SidebarGroup>\n\n        {/* Planning */}\n        <SidebarGroup>\n          <SidebarGroupLabel className=\"text-[10px] uppercase tracking-wider text-muted-foreground/60 font-semibold px-3 mb-0.5 sidebar-label\">Plan</SidebarGroupLabel>\n          <SidebarGroupContent>\n            <SidebarMenu>\n              <SidebarMenuItem>\n                <Collapsible defaultOpen className=\"group w-full min-w-0\">\n                  <div className=\"relative w-full\">\n                    <SidebarMenuButton asChild isActive={location.pathname.startsWith(\"/planning\")} className=\"pr-9\">\n                      <Link to=\"/planning\" className=\"gap-2\">\n                        <Lightbulb size={ICON_SIZE} strokeWidth={ICON_STROKE} className=\"shrink-0\" />\n                        <span className=\"sidebar-label truncate\">Financial Planning</span>\n                      </Link>\n                    </SidebarMenuButton>\n                    <CollapsibleTrigger className={collapsibleChevronTriggerClass} aria-label=\"Toggle financial planning menu\">\n                      <ChevronRight className={collapsibleChevronIconClass} strokeWidth={ICON_STROKE} />\n                    </CollapsibleTrigger>\n                  </div>\n                  <CollapsibleContent className=\"sidebar-label overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\">\n                    <div className=\"mt-1 space-y-0.5 border-l border-sidebar-border/50 pl-2.5 ml-2 py-0.5\">\n                      <SidebarMenu className=\"gap-0.5\">\n                        {planningChildren.map((item) => (\n                          <SidebarMenuItem key={item.url}>\n                            <SidebarMenuButton asChild size=\"sm\" isActive={location.pathname.startsWith(item.url)}>\n                              <NavLink to={item.url}>{item.title}</NavLink>\n                            </SidebarMenuButton>\n                          </SidebarMenuItem>\n                        ))}\n                      </SidebarMenu>\n                    </div>\n                  </CollapsibleContent>\n                </Collapsible>\n              </SidebarMenuItem>\n            </SidebarMenu>\n          </SidebarGroupContent>\n        </SidebarGroup>\n\n        {/* Life, beyond finances */}\n        <SidebarGroup>\n          <SidebarGroupLabel className=\"text-[10px] uppercase tracking-wider text-muted-foreground/60 font-semibold px-3 mb-0.5 sidebar-label\">Life</SidebarGroupLabel>\n          <SidebarGroupContent>\n            {renderNavItems(lifeNavItems)}\n          </SidebarGroupContent>\n        </SidebarGroup>\n      </SidebarContent>\n\n      {/* Footer: User avatar + Notification bell */}\n      <SidebarFooter className=\"p-2 border-t border-sidebar-border/30\">\n        <div className={cn(\"flex items-center gap-2\", collapsed && \"flex-col\")}>\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <button className={cn(\n                \"flex items-center gap-3 flex-1 min-w-0 px-2 py-2 rounded-lg text-sm text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors\",\n                collapsed && \"justify-center px-0 w-full\"\n              )}>\n                <div className=\"h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0 text-xs font-semibold uppercase text-primary\">\n                  {avatarInitial}\n                </div>\n                <div className=\"flex flex-col min-w-0 text-left sidebar-label\">\n                  <span className=\"font-medium truncate leading-tight\">{displayName}</span>\n                  {user?.email && (\n                    <span className=\"text-xs text-muted-foreground truncate leading-tight\">{user.isDemo ? \"Read-only demo\" : user.email}</span>\n                  )}\n                </div>\n                <ChevronsUpDown size={14} strokeWidth={ICON_STROKE} className=\"ml-auto shrink-0 text-muted-foreground sidebar-label\" />\n              </button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent side=\"top\" align=\"start\" className=\"w-60\">\n              <DropdownMenuLabel className=\"flex flex-col gap-0.5\">\n                <span className=\"text-sm font-medium truncate\">{displayName}</span>\n                {user?.email && (\n                  <span className=\"text-xs font-normal text-muted-foreground truncate\">{user.isDemo ? \"Read-only demo\" : user.email}</span>\n                )}\n              </DropdownMenuLabel>\n              <DropdownMenuSeparator />\n\n              <DropdownMenuItem className=\"gap-2\" onClick={() => navigate(\"/settings/account\")}>\n                <UserCircle size={16} strokeWidth={ICON_STROKE} /> Profile\n              </DropdownMenuItem>\n              <DropdownMenuItem className=\"gap-2\" onClick={() => navigate(\"/notifications\")}>\n                <Bell size={16} strokeWidth={ICON_STROKE} /> Notifications\n                {unreadCount > 0 && (\n                  <span className=\"ml-auto rounded-full bg-chart-red/15 px-1.5 text-[11px] font-medium tabular-nums text-chart-red\">\n                    {unreadCount}\n                  </span>\n                )}\n              </DropdownMenuItem>\n              <DropdownMenuItem className=\"gap-2\" onClick={() => navigate(\"/settings/sources\")}>\n                <Database size={16} strokeWidth={ICON_STROKE} /> Data Sources\n              </DropdownMenuItem>\n              <DropdownMenuItem className=\"gap-2\" onClick={() => navigate(\"/settings/brokerages\")}>\n                <Link2 size={16} strokeWidth={ICON_STROKE} /> Brokerages\n              </DropdownMenuItem>\n              <DropdownMenuItem className=\"gap-2\" onClick={() => navigate(\"/settings/preferences\")}>\n                <Sliders size={16} strokeWidth={ICON_STROKE} /> Preferences\n              </DropdownMenuItem>\n              <DropdownMenuItem className=\"gap-2\" onClick={() => navigate(\"/settings/api-access\")}>\n                <KeyRound size={16} strokeWidth={ICON_STROKE} /> API Access\n              </DropdownMenuItem>\n\n              <DropdownMenuSeparator />\n\n              <DropdownMenuSub>\n                <DropdownMenuSubTrigger className=\"gap-2\">\n                  <Sun size={16} strokeWidth={ICON_STROKE} /> Appearance\n                </DropdownMenuSubTrigger>\n                <DropdownMenuSubContent>\n                  <DropdownMenuRadioGroup value={theme} onValueChange={() => toggleTheme()}>\n                    <DropdownMenuRadioItem value=\"light\" className=\"gap-2\" onSelect={(e) => e.preventDefault()}>\n                      <Sun size={14} strokeWidth={ICON_STROKE} /> Light\n                    </DropdownMenuRadioItem>\n                    <DropdownMenuRadioItem value=\"dark\" className=\"gap-2\" onSelect={(e) => e.preventDefault()}>\n                      <Moon size={14} strokeWidth={ICON_STROKE} /> Dark\n                    </DropdownMenuRadioItem>\n                  </DropdownMenuRadioGroup>\n                </DropdownMenuSubContent>\n              </DropdownMenuSub>\n\n              <DropdownMenuSeparator />\n\n              <DropdownMenuItem\n                className=\"gap-2 text-destructive focus:text-destructive\"\n                onClick={() => logout()}\n              >\n                <LogOut size={16} strokeWidth={ICON_STROKE} /> Sign out\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n\n          {/* Notification bell with popover panel */}\n          <Popover>\n            <PopoverTrigger asChild>\n              <button\n                className={cn(\n                  \"relative p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-sidebar-accent transition-colors shrink-0\",\n                  location.pathname === \"/notifications\" && \"text-foreground bg-sidebar-accent\"\n                )}\n                aria-label={unreadCount > 0 ? `Notifications, ${unreadCount} need attention` : \"Notifications\"}\n              >\n                <Bell size={16} strokeWidth={ICON_STROKE} />\n                {unreadCount > 0 && (\n                  <span className=\"absolute top-1 right-1 h-2 w-2 rounded-full bg-chart-red\" aria-hidden />\n                )}\n              </button>\n            </PopoverTrigger>\n            <PopoverContent side=\"right\" align=\"end\" sideOffset={8} className=\"w-80 p-0\">\n              <div className=\"flex items-center justify-between px-4 py-3 border-b border-border/50\">\n                <h4 className=\"text-sm font-semibold\">Alerts</h4>\n                <button\n                  onClick={() => navigate(\"/notifications\")}\n                  className=\"p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors\"\n                  aria-label=\"View all notifications\"\n                >\n                  <ExternalLink size={14} strokeWidth={ICON_STROKE} />\n                </button>\n              </div>\n              {notifications.length === 0 ? (\n                <div className=\"px-4 py-6 text-center\">\n                  <Bell size={24} strokeWidth={1.5} className=\"text-muted-foreground/40 mx-auto mb-2\" />\n                  <p className=\"text-sm text-muted-foreground\">Nothing needs attention</p>\n                </div>\n              ) : (\n                <div className=\"max-h-80 overflow-y-auto py-1\">\n                  {notifications.slice(0, 6).map((n) => (\n                    <Link\n                      key={n.id}\n                      to={n.href}\n                      className=\"flex items-start gap-2.5 px-4 py-2.5 hover:bg-muted/50 transition-colors\"\n                    >\n                      <span\n                        className={cn(\n                          \"mt-1.5 h-1.5 w-1.5 rounded-full shrink-0\",\n                          n.level === \"critical\" ? \"bg-chart-red\" : n.level === \"warning\" ? \"bg-chart-yellow\" : \"bg-primary\"\n                        )}\n                        aria-hidden\n                      />\n                      <div className=\"min-w-0 flex-1\">\n                        <p className=\"text-sm text-foreground truncate\">{n.title}</p>\n                        <p className=\"text-xs text-muted-foreground truncate\">{n.detail}</p>\n                        <p className=\"text-[11px] text-muted-foreground/70 mt-0.5 tabular-nums\">\n                          {formatDate(n.date, dateFormat, \"short\")}\n                        </p>\n                      </div>\n                    </Link>\n                  ))}\n                  {notifications.length > 6 && (\n                    <button\n                      onClick={() => navigate(\"/notifications\")}\n                      className=\"block w-full px-4 py-2 text-xs text-primary hover:text-primary/80 text-center border-t border-border/50 mt-1\"\n                    >\n                      View all {notifications.length}\n                    </button>\n                  )}\n                </div>\n              )}\n\n              {/* Every create/update/delete + login, not just the alerts\n                  above - a quick \"did that actually save\" glance without\n                  leaving the current page. Full history lives on the\n                  Activity tab, this is only ever the last few. */}\n              <div className=\"border-t border-border/50\">\n                <div className=\"flex items-center gap-1.5 px-4 pt-2.5 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/70\">\n                  <History size={11} strokeWidth={1.75} /> Recent activity\n                </div>\n                {recentActivity.length === 0 ? (\n                  <p className=\"px-4 pb-3 text-xs text-muted-foreground\">Nothing yet</p>\n                ) : (\n                  <div className=\"pb-1\">\n                    {/* Read-only, like the full Activity tab: a log entry\n                        isn't a link to anything useful. */}\n                    {recentActivity.map((a) => (\n                      <div\n                        key={a.id}\n                        className=\"flex items-baseline justify-between gap-3 px-4 py-1.5\"\n                      >\n                        <p className=\"text-xs text-foreground truncate\">{activityTitle(a)}</p>\n                        <span className=\"text-[10px] text-muted-foreground/70 tabular-nums shrink-0\">\n                          {a.at ? formatDistanceToNow(new Date(a.at), { addSuffix: true }) : \"\"}\n                        </span>\n                      </div>\n                    ))}\n                  </div>\n                )}\n                <button\n                  onClick={() => navigate(\"/notifications?tab=activity\")}\n                  className=\"block w-full px-4 py-2 text-xs text-primary hover:text-primary/80 text-center border-t border-border/50\"\n                >\n                  View all activity\n                </button>\n              </div>\n            </PopoverContent>\n          </Popover>\n        </div>\n      </SidebarFooter>\n    </Sidebar>\n  );\n}\n"
    },
    {
      "name": "src/components/CommandPalette.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useState } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { Eye, EyeOff, Keyboard, Moon, PiggyBank, Search, Sun } from \"lucide-react\";\nimport { PAGES, OPEN_PALETTE_EVENT } from \"./commandPaletteConfig\";\nimport { Dialog, DialogContent, DialogDescription, DialogTitle } from \"@/components/ui/dialog\";\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from \"@/components/ui/command\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { useTheme } from \"@/context/ThemeContext\";\nimport { CATEGORIES } from \"@/lib/categories\";\nimport { toggleAmountsHidden, usePrivacy } from \"@/lib/privacy\";\nimport { useHotkeys, MOD_LABEL } from \"@/hooks/useHotkeys\";\nimport { CategoryIcon } from \"@/components/elements/CategoryIcon\";\nimport { accountDisplayName, canHoldTransactions } from \"@/lib/accounts\";\n\n\nfunction paletteFilter(value: string, search: string) {\n  const v = value.toLowerCase();\n  const q = search.trim().toLowerCase();\n  if (!q) return 1;\n  if (v.split(/[\\s&/-]+/).some((w) => w.startsWith(q))) return 2;\n  return v.includes(q) ? 1 : 0;\n}\n\nconst keyLabel = (keys: string) => keys.split(\" \").map((k) => k.toUpperCase()).join(\" \");\n\n/**\n * ⌘K palette plus the app-wide shortcuts: jump to a page, an account's or a\n * category's transactions, search transactions, hide amounts, switch theme.\n * Mounted once per app page by Layout.\n */\nexport function CommandPalette() {\n  const navigate = useNavigate();\n  const { accounts: allAccounts } = useFinanceData();\n  const accounts = allAccounts.filter((a) => canHoldTransactions(a.type));\n  const { theme, toggleTheme } = useTheme();\n  const hidden = usePrivacy();\n  const [open, setOpen] = useState(false);\n  const [helpOpen, setHelpOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n\n  useEffect(() => {\n    const onOpen = () => setOpen(true);\n    window.addEventListener(OPEN_PALETTE_EVENT, onOpen);\n    return () => window.removeEventListener(OPEN_PALETTE_EVENT, onOpen);\n  }, []);\n\n  useHotkeys({\n    \"mod+k\": () => setOpen((o) => !o),\n    \"shift+h\": () => toggleAmountsHidden(),\n    \"?\": () => setHelpOpen(true),\n    ...Object.fromEntries(PAGES.map((p) => [p.keys, () => navigate(p.to)])),\n  });\n\n  const run = (fn: () => void) => {\n    setOpen(false);\n    setQuery(\"\");\n    fn();\n  };\n  const q = query.trim();\n\n  return (\n    <>\n      <Dialog open={open} onOpenChange={(o) => { setOpen(o); if (!o) setQuery(\"\"); }}>\n        <DialogContent className=\"overflow-hidden p-0 shadow-lg max-w-lg\">\n          <DialogTitle className=\"sr-only\">Command palette</DialogTitle>\n          <DialogDescription className=\"sr-only\">Jump to a page, account or category, or run an action.</DialogDescription>\n          <Command\n            // Word-start matches rank above other substring matches; nothing\n            // else shows. cmdk's default fuzzy scorer listed \"Personal Care\"\n            // and \"Fees & Charges\" for \"groc\".\n            filter={paletteFilter}\n            className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-2.5\">\n            <CommandInput placeholder=\"Search pages, accounts, categories…\" value={query} onValueChange={setQuery} />\n            <CommandList className=\"max-h-[380px]\">\n              <CommandEmpty>Nothing matches.</CommandEmpty>\n              <CommandGroup heading=\"Go to\">\n                {PAGES.map((p) => (\n                  <CommandItem key={p.to} value={`page ${p.label}`} onSelect={() => run(() => navigate(p.to))} className=\"gap-2\">\n                    <p.icon className=\"h-4 w-4 text-muted-foreground\" />\n                    <span>{p.label}</span>\n                    <CommandShortcut>{keyLabel(p.keys)}</CommandShortcut>\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n              <CommandGroup heading=\"Actions\">\n                <CommandItem value=\"action hide show amounts privacy\" onSelect={() => run(toggleAmountsHidden)} className=\"gap-2\">\n                  {hidden ? <Eye className=\"h-4 w-4 text-muted-foreground\" /> : <EyeOff className=\"h-4 w-4 text-muted-foreground\" />}\n                  <span>{hidden ? \"Show amounts\" : \"Hide amounts\"}</span>\n                  <CommandShortcut>⇧H</CommandShortcut>\n                </CommandItem>\n                <CommandItem value=\"action theme dark light mode\" onSelect={() => run(toggleTheme)} className=\"gap-2\">\n                  {theme === \"dark\" ? <Sun className=\"h-4 w-4 text-muted-foreground\" /> : <Moon className=\"h-4 w-4 text-muted-foreground\" />}\n                  <span>Switch to {theme === \"dark\" ? \"light\" : \"dark\"} theme</span>\n                </CommandItem>\n                <CommandItem value=\"action keyboard shortcuts help\" onSelect={() => run(() => setHelpOpen(true))} className=\"gap-2\">\n                  <Keyboard className=\"h-4 w-4 text-muted-foreground\" />\n                  <span>Keyboard shortcuts</span>\n                  <CommandShortcut>?</CommandShortcut>\n                </CommandItem>\n              </CommandGroup>\n              {accounts.length > 0 && (\n                <>\n                  <CommandSeparator />\n                  <CommandGroup heading=\"Accounts\">\n                    {accounts.map((a) => (\n                      <CommandItem key={a.id} value={`account ${accountDisplayName(a)} ${a.institution}`} onSelect={() => run(() => navigate(`/transactions?account=${encodeURIComponent(a.id)}`))} className=\"gap-2\">\n                        <PiggyBank className=\"h-4 w-4 text-muted-foreground\" />\n                        <span className=\"truncate\">{accountDisplayName(a)}</span>\n                        <span className=\"text-xs text-muted-foreground truncate\">{a.institution}</span>\n                      </CommandItem>\n                    ))}\n                  </CommandGroup>\n                </>\n              )}\n              <CommandSeparator />\n              <CommandGroup heading=\"Categories\">\n                {CATEGORIES.map((c) => (\n                  <CommandItem key={c} value={`category ${c}`} onSelect={() => run(() => navigate(`/transactions?category=${encodeURIComponent(c)}`))} className=\"gap-2\">\n                    <CategoryIcon category={c} size=\"xs\" />\n                    <span>{c}</span>\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n              {/* Always offered, but its value doesn't contain the query, so it ranks\n                  below any real match: \"groceries\" + Enter opens the category, while a\n                  merchant name with no other match searches for it. */}\n              {q && (\n                <CommandGroup heading=\"Transactions\">\n                  <CommandItem forceMount value=\"search transactions\" onSelect={() => run(() => navigate(`/transactions?q=${encodeURIComponent(q)}`))} className=\"gap-2\">\n                    <Search className=\"h-4 w-4 text-muted-foreground\" />\n                    <span>Search transactions for “{q}”</span>\n                  </CommandItem>\n                </CommandGroup>\n              )}\n            </CommandList>\n          </Command>\n        </DialogContent>\n      </Dialog>\n      <ShortcutsHelp open={helpOpen} onOpenChange={setHelpOpen} />\n    </>\n  );\n}\n\nconst SHORTCUTS: { group: string; items: [string, string][] }[] = [\n  { group: \"Anywhere\", items: [[`${MOD_LABEL} K`, \"Command palette\"], [\"⇧ H\", \"Hide or show amounts\"], [\"?\", \"This list\"]] },\n  { group: \"Go to\", items: PAGES.map((p) => [keyLabel(p.keys), p.label] as [string, string]) },\n  { group: \"Transactions\", items: [[\"/\", \"Search\"], [\"J / K\", \"Next / previous row\"], [\"Enter\", \"Open row\"], [\"X\", \"Select row\"], [\"Esc\", \"Clear search\"]] },\n];\n\nfunction ShortcutsHelp({ open, onOpenChange }: { open: boolean; onOpenChange: (o: boolean) => void }) {\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogTitle>Keyboard shortcuts</DialogTitle>\n        <DialogDescription className=\"sr-only\">Every keyboard shortcut in the app.</DialogDescription>\n        <div className=\"space-y-4\">\n          {SHORTCUTS.map((g) => (\n            <section key={g.group} className=\"space-y-1.5\">\n              <h3 className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">{g.group}</h3>\n              <dl className=\"space-y-1\">\n                {g.items.map(([keys, what]) => (\n                  <div key={keys} className=\"flex items-center justify-between text-sm\">\n                    <dt className=\"text-foreground\">{what}</dt>\n                    <dd className=\"flex gap-1\">\n                      {keys.split(\" / \").map((k) => (\n                        <kbd key={k} className=\"rounded border border-border bg-muted px-1.5 font-mono text-[11px] leading-5 text-muted-foreground\">{k}</kbd>\n                      ))}\n                    </dd>\n                  </div>\n                ))}\n              </dl>\n            </section>\n          ))}\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\n"
    },
    {
      "name": "src/components/ErrorBoundary.tsx",
      "type": "text/plain",
      "content": "import { reportClientError } from \"@/lib/errorReporting\";\nimport { Component, type ReactNode } from \"react\";\nimport { Layout } from \"@/components/Layout\";\nimport { AlertTriangle, RefreshCw } from \"lucide-react\";\n\ninterface Props {\n  children: ReactNode;\n  fallbackMessage?: string;\n}\n\ninterface State {\n  hasError: boolean;\n  error: Error | null;\n}\n\n/**\n * L-2: Catches rendering errors and shows a friendly fallback UI\n * instead of a white screen. Works as a wrapper around page content.\n */\nexport class ErrorBoundary extends Component<Props, State> {\n  constructor(props: Props) {\n    super(props);\n    this.state = { hasError: false, error: null };\n  }\n\n  static getDerivedStateFromError(error: Error): State {\n    return { hasError: true, error };\n  }\n\n  componentDidCatch(error: Error, info: React.ErrorInfo) {\n    console.error(\"[ErrorBoundary]\", error, info.componentStack);\n    reportClientError(\"react\", error);\n  }\n\n  handleReset = () => {\n    this.setState({ hasError: false, error: null });\n  };\n\n  render() {\n    if (this.state.hasError) {\n      return (\n        <div className=\"flex flex-col items-center justify-center min-h-[400px] gap-6 p-8\">\n          <div className=\"h-20 w-20 rounded-full bg-chart-red/10 border border-chart-red/20 flex items-center justify-center\">\n            <AlertTriangle size={32} strokeWidth={1.5} className=\"text-chart-red\" />\n          </div>\n          <div className=\"text-center space-y-2 max-w-md\">\n            <h2 className=\"text-xl font-bold text-foreground\">Something went wrong</h2>\n            <p className=\"text-sm text-muted-foreground leading-relaxed\">\n              {this.props.fallbackMessage || \"An unexpected error occurred while rendering this page. Please try again.\"}\n            </p>\n            {this.state.error && (\n              <details className=\"mt-3 text-left\">\n                <summary className=\"text-xs text-muted-foreground cursor-pointer hover:text-foreground transition-colors\">\n                  Technical details\n                </summary>\n                <pre className=\"mt-2 p-3 bg-muted rounded-lg text-[11px] text-muted-foreground overflow-auto max-h-32 font-mono\">\n                  {this.state.error.message}\n                </pre>\n              </details>\n            )}\n          </div>\n          <button\n            onClick={this.handleReset}\n            className=\"flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors\"\n          >\n            <RefreshCw size={14} strokeWidth={1.5} />\n            Try Again\n          </button>\n        </div>\n      );\n    }\n\n    return this.props.children;\n  }\n}\n"
    },
    {
      "name": "src/components/Layout.tsx",
      "type": "text/plain",
      "content": "import { DASHBOARD_PATH } from \"@/lib/deployment\";\nimport { SidebarProvider, SidebarTrigger } from \"@/components/ui/sidebar\";\nimport { AppSidebar } from \"@/components/AppSidebar\";\nimport { PageTransition } from \"@/components/PageTransition\";\nimport { useLocation, Link } from \"react-router-dom\";\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { normalizeMerchant } from \"@/lib/merchant\";\nimport React from \"react\";\nimport { Eye, EyeOff, Search } from \"lucide-react\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { CommandPalette } from \"@/components/CommandPalette\";\nimport { OPEN_PALETTE_EVENT } from \"@/components/commandPaletteConfig\";\nimport { isMac, MOD_LABEL } from \"@/hooks/useHotkeys\";\nimport { toggleAmountsHidden, usePrivacy } from \"@/lib/privacy\";\nimport { accountDisplayName } from \"@/lib/accounts\";\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nfunction DynamicBreadcrumbs() {\n  const location = useLocation();\n  const { investmentAccounts, accounts, transactions } = useFinanceData();\n  const paths = location.pathname.split(\"/\").filter(Boolean);\n\n  // Resolve an opaque id segment (e.g. /investments/<uuid>) to a human label so\n  // the breadcrumb shows the account name instead of a raw UUID.\n  const labelFor = (segment: string, index: number) => {\n    const acct = investmentAccounts.find((a) => a.id === segment);\n    if (acct) return acct.label;\n\n    if (index > 0 && paths[index - 1] === \"accounts\") {\n      const account = accounts.find((a) => a.id === segment);\n      if (account) return accountDisplayName(account);\n    }\n\n    // A transaction id is the child of /transactions. Its ids look like\n    // `plaid-t-0-3-2`, which the generic title-caser turned into the nonsense\n    // \"Plaid T 0 3 2\", show the merchant, or a plain label when the row is not\n    // loaded (the list is server-paged, so it often is not).\n    if (index > 0 && paths[index - 1] === \"transactions\") {\n      const txn = transactions.find((t) => t.id === segment);\n      return txn ? normalizeMerchant(txn.description) : \"Transaction\";\n    }\n\n    if (UUID_RE.test(segment) || /^[0-9a-f]{16,}$/i.test(segment)) return \"Details\";\n    return segment.split(\"-\").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(\" \");\n  };\n  \n  if (location.pathname === DASHBOARD_PATH || paths.length === 0) {\n    return (\n      <Breadcrumb>\n        <BreadcrumbList>\n          <BreadcrumbItem>\n            <BreadcrumbPage>Overview</BreadcrumbPage>\n          </BreadcrumbItem>\n        </BreadcrumbList>\n      </Breadcrumb>\n    );\n  }\n\n  return (\n    <Breadcrumb>\n      <BreadcrumbList>\n        <BreadcrumbItem className=\"hidden md:block\">\n          <BreadcrumbLink asChild>\n            <Link to={DASHBOARD_PATH}>Overview</Link>\n          </BreadcrumbLink>\n        </BreadcrumbItem>\n        \n        {paths.map((path, index) => {\n          const isLast = index === paths.length - 1;\n          const to = `/${paths.slice(0, index + 1).join(\"/\")}`;\n          const title = labelFor(path, index);\n          \n          return (\n            <React.Fragment key={to}>\n              <BreadcrumbSeparator className=\"hidden md:block\" />\n              <BreadcrumbItem>\n                {isLast ? (\n                  <BreadcrumbPage>{title}</BreadcrumbPage>\n                ) : (\n                  <BreadcrumbLink asChild className=\"hidden md:block\">\n                    <Link to={to}>{title}</Link>\n                  </BreadcrumbLink>\n                )}\n              </BreadcrumbItem>\n            </React.Fragment>\n          );\n        })}\n      </BreadcrumbList>\n    </Breadcrumb>\n  );\n}\n\n/** Right side of the top bar: open the command palette, hide amounts. */\nfunction TopBarActions() {\n  const hidden = usePrivacy();\n  return (\n    <div className=\"ml-auto flex items-center gap-1\">\n      <button\n        type=\"button\"\n        onClick={() => window.dispatchEvent(new Event(OPEN_PALETTE_EVENT))}\n        aria-keyshortcuts={`${isMac ? \"Meta\" : \"Control\"}+K`}\n        className=\"flex h-8 items-center gap-2 rounded-md border border-border/60 bg-background px-2.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n      >\n        <Search className=\"h-3.5 w-3.5\" />\n        <span className=\"hidden sm:inline\">Search…</span>\n        <kbd className=\"hidden sm:inline rounded border border-border bg-muted px-1 font-mono text-[10px] leading-4\">{MOD_LABEL}K</kbd>\n      </button>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <button\n            type=\"button\"\n            onClick={toggleAmountsHidden}\n            aria-pressed={hidden}\n            aria-label={hidden ? \"Show amounts\" : \"Hide amounts\"}\n            aria-keyshortcuts=\"Shift+H\"\n            className=\"inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n          >\n            {hidden ? <EyeOff className=\"h-4 w-4\" /> : <Eye className=\"h-4 w-4\" />}\n          </button>\n        </TooltipTrigger>\n        <TooltipContent side=\"bottom\">{hidden ? \"Show amounts\" : \"Hide amounts\"} <span className=\"ml-1 opacity-60\">⇧H</span></TooltipContent>\n      </Tooltip>\n    </div>\n  );\n}\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <SidebarProvider>\n      <div className=\"min-h-screen flex w-full\">\n        <AppSidebar />\n        <div className=\"flex-1 flex flex-col min-w-0\">\n          <header className=\"h-12 flex items-center gap-2 px-4 shrink-0 border-b border-border/30 bg-background/80 backdrop-blur-sm sticky top-0 z-10 print:hidden\">\n            <div className=\"lg:hidden\">\n              <SidebarTrigger />\n            </div>\n            <DynamicBreadcrumbs />\n            <TopBarActions />\n          </header>\n          <main id=\"scroll-container\" className=\"flex-1 overflow-x-clip p-4 md:p-6 lg:p-8 relative print:overflow-visible print:p-0\">\n            <PageTransition>{children}</PageTransition>\n          </main>\n          <CommandPalette />\n        </div>\n      </div>\n    </SidebarProvider>\n  );\n}\n"
    },
    {
      "name": "src/components/NavLink.tsx",
      "type": "text/plain",
      "content": "import { NavLink as RouterNavLink, NavLinkProps } from \"react-router-dom\";\nimport { forwardRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface NavLinkCompatProps extends Omit<NavLinkProps, \"className\"> {\n  className?: string;\n  activeClassName?: string;\n  pendingClassName?: string;\n}\n\nconst NavLink = forwardRef<HTMLAnchorElement, NavLinkCompatProps>(\n  ({ className, activeClassName, pendingClassName, to, ...props }, ref) => {\n    return (\n      <RouterNavLink\n        ref={ref}\n        to={to}\n        className={({ isActive, isPending }) =>\n          cn(className, isActive && activeClassName, isPending && pendingClassName)\n        }\n        {...props}\n      />\n    );\n  },\n);\n\nNavLink.displayName = \"NavLink\";\n\nexport { NavLink };\n"
    },
    {
      "name": "src/components/PageTransition.tsx",
      "type": "text/plain",
      "content": "import { useLocation } from \"react-router-dom\";\n\nexport function PageTransition({ children }: { children: React.ReactNode }) {\n  const location = useLocation();\n  return (\n    <div key={location.pathname} className=\"animate-page-in\">\n      {children}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/ScrollToTop.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useRef } from \"react\";\nimport { useLocation } from \"react-router-dom\";\n\n/**\n * `Layout`'s `<main>` carries `overflow-auto`, but its ancestor chain uses\n * `min-h-screen` rather than a hard `h-screen`, so on a page taller than the\n * viewport `<main>` just grows to fit its content instead of clipping it —\n * the window/document scrolls, not that element. Reset both: whichever one\n * actually owns the scroll on a given page, the other is a no-op.\n */\nconst SCROLL_CONTAINER_ID = \"scroll-container\";\n\n/** First path segment, e.g. \"/transactions/abc123\" -> \"transactions\". */\nfunction section(pathname: string): string {\n  return pathname.split(\"/\")[1] ?? \"\";\n}\n\n/**\n * Resets scroll to the top when navigating to a different section of the\n * app (e.g. Budgets -> Reports), which plain `<BrowserRouter>` never does on\n * its own — leaving a page scrolled mid-way exactly where you left it the\n * next time you open it, even after visiting somewhere else entirely.\n *\n * Deliberately keyed on the URL's first path segment rather than the whole\n * location: navigating within the same section — a query-string filter\n * (`/transactions?category=X`) or a path param for a detail view\n * (`/transactions/:id`) — is \"the same page\" with more state, not a new one,\n * and should keep the list scrolled where the user was reading it.\n */\nexport function ScrollToTop() {\n  const { pathname } = useLocation();\n  const prevSection = useRef<string | null>(null);\n\n  useEffect(() => {\n    const current = section(pathname);\n    if (prevSection.current !== null && prevSection.current !== current) {\n      window.scrollTo(0, 0);\n      document.getElementById(SCROLL_CONTAINER_ID)?.scrollTo(0, 0);\n    }\n    prevSection.current = current;\n  }, [pathname]);\n\n  return null;\n}\n"
    },
    {
      "name": "src/components/SettingsLayout.tsx",
      "type": "text/plain",
      "content": "import React from \"react\";\nimport { useLocation, useNavigate } from \"react-router-dom\";\nimport { Layout } from \"@/components/Layout\";\nimport { NavLink } from \"@/components/NavLink\";\nimport { UserCircle, Sliders, Database, Link2, Wand2, Tag, Store, Repeat, List, KeyRound } from \"lucide-react\";\n\nconst tabs = [\n  { label: \"Account\", to: \"/settings/account\", icon: UserCircle },\n  { label: \"Preferences\", to: \"/settings/preferences\", icon: Sliders },\n  { label: \"Data Sources\", to: \"/settings/sources\", icon: Database },\n  { label: \"Brokerages\", to: \"/settings/brokerages\", icon: Link2 },\n  { label: \"Rules\", to: \"/settings/rules\", icon: Wand2 },\n  { label: \"Tags\", to: \"/settings/tags\", icon: Tag },\n  { label: \"Merchants\", to: \"/settings/merchants\", icon: Store },\n  { label: \"Recurring\", to: \"/settings/recurring\", icon: Repeat },\n  { label: \"Categories\", to: \"/settings/categories\", icon: List },\n  { label: \"API Access\", to: \"/settings/api-access\", icon: KeyRound },\n];\n\n/**\n * Shared shell for the /settings/* pages: a single \"Settings\" header plus a tab\n * row, so the previously disconnected settings routes read as one coherent area.\n */\nexport function SettingsLayout({ children }: { children: React.ReactNode }) {\n  const navigate = useNavigate();\n  const { pathname } = useLocation();\n  // \"/settings\" itself shows Account; unknown subpaths fall back to it too.\n  const current = tabs.find((t) => pathname === t.to || pathname.startsWith(`${t.to}/`))?.to ?? tabs[0].to;\n  return (\n    <Layout>\n      <div className=\"space-y-6\">\n        <div>\n          <h1 className=\"text-2xl font-bold text-foreground tracking-tight\">Settings</h1>\n          <p className=\"text-sm text-muted-foreground mt-1\">\n            Manage your account, data connections, and preferences\n          </p>\n        </div>\n\n        {/* Below lg the ten tabs can't fit and a hidden-scrollbar strip hid the\n            last ones; a native select is always reachable and works with a\n            thumb. From lg up the tabs wrap onto a second row instead of scrolling. */}\n        <label className=\"block lg:hidden\">\n          <span className=\"sr-only\">Settings section</span>\n          <select\n            value={current}\n            onChange={(e) => navigate(e.target.value)}\n            className=\"h-11 w-full rounded-lg border border-input bg-background px-3 text-sm font-medium text-foreground\"\n          >\n            {tabs.map((t) => (\n              <option key={t.to} value={t.to}>{t.label}</option>\n            ))}\n          </select>\n        </label>\n\n        <nav aria-label=\"Settings sections\" className=\"hidden flex-wrap items-center gap-1 border-b border-border pb-2 lg:flex\">\n          {tabs.map((t) => (\n            <NavLink\n              key={t.to}\n              to={t.to}\n              className=\"flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground whitespace-nowrap\"\n              activeClassName=\"bg-muted text-foreground\"\n            >\n              <t.icon className=\"h-4 w-4\" />\n              {t.label}\n            </NavLink>\n          ))}\n        </nav>\n\n        <div>{children}</div>\n      </div>\n    </Layout>\n  );\n}\n"
    },
    {
      "name": "src/components/TransactionDetail.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useState } from \"react\";\nimport { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from \"@/components/ui/sheet\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Link } from \"react-router-dom\";\nimport { Building2 } from \"lucide-react\";\nimport { Transaction } from \"@/types/finance\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { normalizeMerchant } from \"@/lib/merchant\";\nimport { CategoryBadge } from \"@/components/elements/CategoryIcon\";\nimport { CategoryPicker } from \"@/components/elements/CategoryPicker\";\nimport { DetailRow } from \"@/components/elements/DetailRow\";\nimport { TagPicker } from \"@/components/elements/TagPicker\";\nimport { SplitEditor } from \"@/components/elements/SplitEditor\";\nimport { useTags, useTagMutations, useTransactionTags } from \"@/hooks/useTags\";\nimport { useTransactionSplits } from \"@/hooks/useSplits\";\nimport { useTransactionAccountName } from \"@/hooks/useAccountNames\";\n\nexport const allCategories = [\n  \"Food & Dining\", \"Shopping\", \"Transportation\", \"Entertainment\",\n  \"Bills & Utilities\", \"Health\", \"Travel\", \"Income\", \"Transfer\", \"Groceries\",\n];\n\ninterface TransactionDetailProps {\n  transaction: Transaction | null;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  onCategoryChange?: (txnId: string, newCategory: string) => void;\n  onNoteChange?: (txnId: string, note: string) => Promise<void> | void;\n  /** Called after a split is saved or removed, so the caller can refresh\n   * whatever list it's showing this transaction in (its own isSplit/\n   * splitCount there won't otherwise update until the next fetch). */\n  onSplitsChange?: (txnId: string) => void;\n  categories?: string[];\n}\n\nexport function TransactionDetail({\n  transaction, open, onOpenChange, onCategoryChange, onNoteChange, onSplitsChange, categories,\n}: TransactionDetailProps) {\n  // Hooks must run unconditionally, so the null guard comes after them.\n  const { moneyAbs, date: fmtDate } = useMoney({ accountId: transaction?.accountId });\n  const [note, setNote] = useState(\"\");\n  const [savingNote, setSavingNote] = useState(false);\n  const { data: allTags = [] } = useTags();\n  const { data: txnTags = [], setTags } = useTransactionTags(transaction?.id);\n  const { create: createTag } = useTagMutations();\n  const txnTagIds = txnTags.map((t) => t.id);\n  const { data: splits = [], setSplits } = useTransactionSplits(transaction?.id);\n  const [splitEditorOpen, setSplitEditorOpen] = useState(false);\n  const txnAccountName = useTransactionAccountName();\n\n  useEffect(() => {\n    setNote(transaction?.note ?? \"\");\n  }, [transaction]);\n\n  if (!transaction) return null;\n\n  const noteDirty = note !== (transaction.note ?? \"\");\n  const saveNote = async () => {\n    if (!noteDirty || !onNoteChange) return;\n    setSavingNote(true);\n    try {\n      await onNoteChange(transaction.id, note);\n    } finally {\n      setSavingNote(false);\n    }\n  };\n\n  const accountName = txnAccountName(transaction);\n  const formattedAmount = `${transaction.amount >= 0 ? \"+\" : \"\\u2212\"}${moneyAbs(transaction.amount)}`;\n\n  return (\n    <>\n    <Sheet open={open} onOpenChange={onOpenChange}>\n      <SheetContent className=\"bg-card sm:max-w-md p-0 border-border/50\">\n        <div className=\"p-6 space-y-6\">\n          {/* Radix requires a title for the panel to be announced to screen\n              readers. It doubles as the visible heading. */}\n          <SheetHeader className=\"space-y-3 pt-2 text-left\">\n            <SheetDescription className=\"text-xs text-muted-foreground tracking-wide\">\n              {accountName}\n            </SheetDescription>\n            <SheetTitle className=\"text-3xl font-bold text-foreground tracking-tight\">\n              {formattedAmount}\n            </SheetTitle>\n          </SheetHeader>\n\n          {/* L-5: View account link */}\n          {transaction.accountId && (\n            <Link\n              to={`/transactions?account=${encodeURIComponent(transaction.accountId)}`}\n              className=\"flex items-center gap-2 px-3 py-2 rounded-lg bg-muted/50 hover:bg-muted text-sm text-muted-foreground hover:text-foreground transition-colors\"\n              onClick={() => onOpenChange(false)}\n            >\n              <Building2 size={14} strokeWidth={1.5} />\n              <span className=\"font-medium\">{accountName || \"View account\"}</span>\n              <span className=\"ml-auto text-xs\">→</span>\n            </Link>\n          )}\n\n          {/* Merchant, then the facts as label/value rows */}\n          <div className=\"space-y-2\">\n            <p className=\"text-sm font-semibold text-foreground\">{normalizeMerchant(transaction.description) || transaction.description}</p>\n            <dl className=\"divide-y divide-border/40\">\n              <DetailRow label=\"Date\">{fmtDate(transaction.date)}</DetailRow>\n              <DetailRow label=\"Type\"><span className=\"capitalize\">{transaction.type}</span></DetailRow>\n              <DetailRow label=\"Category\"><CategoryBadge category={transaction.category} /></DetailRow>\n            </dl>\n          </div>\n\n          <Separator className=\"bg-border/50\" />\n\n          {/* Category Select, or a read-only breakdown once split. Driven by\n              the live splits query rather than transaction.isSplit: that\n              prop is a snapshot taken when the panel opened and doesn't\n              update in place after a save within this same session. */}\n          <div className=\"space-y-2\">\n            <div className=\"flex items-center justify-between\">\n              <p className=\"text-xs text-muted-foreground font-medium uppercase tracking-wider\">Category</p>\n              <button\n                type=\"button\"\n                className=\"text-xs text-muted-foreground hover:text-foreground transition-colors\"\n                onClick={() => setSplitEditorOpen(true)}\n              >\n                {splits.length > 0 ? \"Edit split\" : \"Split\"}\n              </button>\n            </div>\n            {splits.length > 0 ? (\n              <div className=\"space-y-1.5\">\n                {splits.map((s) => (\n                  <div\n                    key={s.id}\n                    className=\"flex items-center justify-between gap-2 text-sm px-3 py-2 rounded-md bg-muted/40 border border-border/50\"\n                  >\n                    <CategoryBadge category={s.category || \"Uncategorized\"} />\n                    <span className=\"font-medium tabular-nums\">{moneyAbs(s.amount)}</span>\n                  </div>\n                ))}\n              </div>\n            ) : (\n              <CategoryPicker\n                value={transaction.category}\n                onChange={(val) => onCategoryChange?.(transaction.id, val)}\n                categories={categories || allCategories}\n                triggerClassName=\"bg-background border-border/50 h-9\"\n              />\n            )}\n          </div>\n\n          <Separator className=\"bg-border/50\" />\n\n          {/* Tags */}\n          <div className=\"space-y-2\">\n            <p className=\"text-xs text-muted-foreground font-medium uppercase tracking-wider\">Tags</p>\n            <TagPicker\n              tags={allTags}\n              selectedIds={txnTagIds}\n              onChange={(ids) => setTags.mutate(ids)}\n              onCreate={(name) => createTag.mutateAsync({ name }).then((t) => t.id)}\n              disabled={setTags.isPending}\n            />\n          </div>\n\n          <Separator className=\"bg-border/50\" />\n\n          {/* Notes, the field was readOnly because there was no column to\n              store it in; it saves on blur now. */}\n          <div className=\"space-y-2\">\n            <p className=\"text-xs text-muted-foreground font-medium uppercase tracking-wider\">Notes</p>\n            <Textarea\n              value={note}\n              onChange={(e) => setNote(e.target.value)}\n              onBlur={() => void saveNote()}\n              placeholder=\"Add a note…\"\n              disabled={!onNoteChange || savingNote}\n              className=\"bg-background border-border/50 text-sm min-h-[72px] resize-none\"\n            />\n            {noteDirty && (\n              <p className=\"text-[11px] text-muted-foreground\">\n                {savingNote ? \"Saving…\" : \"Click outside to save\"}\n              </p>\n            )}\n          </div>\n\n          <Separator className=\"bg-border/50\" />\n\n          {/* Bank Description */}\n          <div className=\"space-y-2\">\n            <p className=\"text-xs text-muted-foreground font-medium uppercase tracking-wider\">Bank description</p>\n            <p className=\"text-sm font-medium text-foreground\">{transaction.description}</p>\n          </div>\n        </div>\n      </SheetContent>\n    </Sheet>\n\n    <SplitEditor\n      open={splitEditorOpen}\n      onOpenChange={setSplitEditorOpen}\n      transactionAmount={transaction.amount}\n      initialSplits={splits}\n      onSave={(rows) => setSplits.mutateAsync(rows).then(() => onSplitsChange?.(transaction.id))}\n      saving={setSplits.isPending}\n    />\n    </>\n  );\n}\n"
    },
    {
      "name": "src/components/accounts/AccountDialog.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useState } from \"react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Button } from \"@/components/ui/button\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { toast } from \"sonner\";\nimport { toDateString } from \"@/lib/format\";\nimport { isDebtAccount, isIlliquidAsset } from \"@/lib/accounts\";\nimport { DatePicker } from \"@/components/elements/DatePicker\";\nimport type { Account } from \"@/types/finance\";\n\n/** The eight the database's CHECK constraint accepts, anything else is a 500. */\nconst ACCOUNT_TYPES: { value: Account[\"type\"]; label: string }[] = [\n  { value: \"checking\", label: \"Checking\" },\n  { value: \"savings\", label: \"Savings\" },\n  { value: \"credit\", label: \"Credit card\" },\n  { value: \"investment\", label: \"Investment\" },\n  { value: \"loan\", label: \"Loan\" },\n  { value: \"vehicle\", label: \"Vehicle\" },\n  { value: \"property\", label: \"Property\" },\n  { value: \"cash\", label: \"Cash on hand\" },\n];\n\nconst PROPERTY_TYPES: { value: NonNullable<Account[\"propertyType\"]>; label: string }[] = [\n  { value: \"primary\", label: \"Primary residence\" },\n  { value: \"rental\", label: \"Rental\" },\n  { value: \"other\", label: \"Other\" },\n];\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  /** Present when editing; absent when adding. */\n  account?: Account | null;\n  onAdd: (a: Account) => Promise<void>;\n  onUpdate: (id: string, updates: Partial<Account>) => Promise<void>;\n  /** Limit the Type picker (the Accounts page offers bank types, the Assets\n   * page the rest). Defaults to all eight. */\n  types?: Account[\"type\"][];\n}\n\n/**\n * Add or edit a manual account.\n *\n * Previously the only editable field anywhere was the nickname, so cash and any\n * institution Plaid does not reach simply could not be represented.\n */\nexport function AccountDialog({ open, onOpenChange, account, onAdd, onUpdate, types }: Props) {\n  const typeOptions = types ? ACCOUNT_TYPES.filter((t) => types.includes(t.value) || t.value === account?.type) : ACCOUNT_TYPES;\n  const editing = !!account;\n  // A Plaid account's figures are owned by the sync and overwritten on the next\n  // run, so only the nickname is editable for those.\n  const plaidManaged = !!account?.id.startsWith(\"plaid-\");\n\n  const [name, setName] = useState(\"\");\n  const [institution, setInstitution] = useState(\"\");\n  const [type, setType] = useState<Account[\"type\"]>(\"checking\");\n  const [balance, setBalance] = useState(\"\");\n  const [creditLimit, setCreditLimit] = useState(\"\");\n  const [mask, setMask] = useState(\"\");\n  const [originalPrincipal, setOriginalPrincipal] = useState(\"\");\n  const [interestRate, setInterestRate] = useState(\"\");\n  const [monthlyPayment, setMonthlyPayment] = useState(\"\");\n  const [termMonths, setTermMonths] = useState(\"\");\n  const [originationDate, setOriginationDate] = useState(\"\");\n  const [purchasePrice, setPurchasePrice] = useState(\"\");\n  const [purchaseDate, setPurchaseDate] = useState(\"\");\n  const [propertyType, setPropertyType] = useState<NonNullable<Account[\"propertyType\"]>>(\"primary\");\n  const [saving, setSaving] = useState(false);\n\n  useEffect(() => {\n    if (!open) return;\n    setName(account?.name ?? \"\");\n    setInstitution(account?.institution ?? \"\");\n    setType(account?.type ?? types?.[0] ?? \"checking\");\n    setBalance(account ? String(account.balance) : \"\");\n    setCreditLimit(account?.creditLimit != null ? String(account.creditLimit) : \"\");\n    setMask(account?.mask ?? \"\");\n    setOriginalPrincipal(account?.originalPrincipal != null ? String(account.originalPrincipal) : \"\");\n    setInterestRate(account?.interestRate != null ? String(account.interestRate) : \"\");\n    setMonthlyPayment(account?.monthlyPayment != null ? String(account.monthlyPayment) : \"\");\n    setTermMonths(account?.termMonths != null ? String(account.termMonths) : \"\");\n    setOriginationDate(account?.originationDate ?? \"\");\n    setPurchasePrice(account?.purchasePrice != null ? String(account.purchasePrice) : \"\");\n    setPurchaseDate(account?.purchaseDate ?? \"\");\n    setPropertyType(account?.propertyType ?? \"primary\");\n  }, [open, account, types]);\n\n  const submit = async () => {\n    const trimmedName = name.trim();\n    if (!trimmedName) { toast.error(\"Give the account a name\"); return; }\n    const parsedBalance = parseFloat(balance);\n    if (Number.isNaN(parsedBalance)) { toast.error(\"Enter a balance\"); return; }\n\n    // A credit card's or loan's balance is what you owe, stored negative so\n    // that summing every account's balance gives net worth.\n    const isDebt = isDebtAccount(type);\n    const signedBalance = isDebt ? -Math.abs(parsedBalance) : parsedBalance;\n    const limit = creditLimit.trim() ? Math.abs(parseFloat(creditLimit)) : null;\n    const loanFields = type === \"loan\" ? {\n      originalPrincipal: originalPrincipal.trim() ? Math.abs(parseFloat(originalPrincipal)) : null,\n      interestRate: interestRate.trim() ? parseFloat(interestRate) : null,\n      monthlyPayment: monthlyPayment.trim() ? Math.abs(parseFloat(monthlyPayment)) : null,\n      termMonths: termMonths.trim() ? Math.round(parseFloat(termMonths)) : null,\n      originationDate: originationDate || null,\n    } : {\n      originalPrincipal: null, interestRate: null, monthlyPayment: null, termMonths: null, originationDate: null,\n    };\n    const assetFields = isIlliquidAsset(type) ? {\n      purchasePrice: purchasePrice.trim() ? Math.abs(parseFloat(purchasePrice)) : null,\n      purchaseDate: purchaseDate || null,\n      propertyType: type === \"property\" ? propertyType : null,\n    } : {\n      purchasePrice: null, purchaseDate: null, propertyType: null,\n    };\n\n    setSaving(true);\n    try {\n      if (editing) {\n        await onUpdate(account!.id, {\n          name: trimmedName,\n          institution: institution.trim() || trimmedName,\n          type,\n          balance: signedBalance,\n          creditLimit: type === \"credit\" ? limit : null,\n          available: type === \"credit\" && limit != null ? limit - Math.abs(signedBalance) : null,\n          mask: mask.trim() || undefined,\n          ...loanFields,\n          ...assetFields,\n        });\n        toast.success(\"Account updated\");\n      } else {\n        await onAdd({\n          // `manual-` keeps it clear of the `plaid-` rows that the Plaid sync\n          // deletes and rebuilds wholesale on every run.\n          id: `manual-acct-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n          name: trimmedName,\n          institution: institution.trim() || trimmedName,\n          type,\n          balance: signedBalance,\n          available: type === \"credit\" && limit != null ? limit - Math.abs(signedBalance) : null,\n          creditLimit: type === \"credit\" ? limit : null,\n          lastSynced: toDateString(new Date()),\n          mask: mask.trim(),\n          ...loanFields,\n          ...assetFields,\n        });\n        toast.success(\"Account added\");\n      }\n      onOpenChange(false);\n    } catch {\n      toast.error(editing ? \"Couldn't update the account\" : \"Couldn't add the account\");\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"sm:max-w-[440px]\">\n        <DialogHeader>\n          <DialogTitle>{editing ? \"Edit account\" : \"Add account\"}</DialogTitle>\n        </DialogHeader>\n\n        {plaidManaged ? (\n          <p className=\"text-sm text-muted-foreground py-2\">\n            This account syncs from your bank, so its balance and details are\n            managed there. You can rename it from the card's pencil icon.\n          </p>\n        ) : (\n          <div className=\"space-y-4 py-2\">\n            <div className=\"space-y-1.5\">\n              <Label>Name</Label>\n              <Input\n                value={name}\n                onChange={(e) => setName(e.target.value)}\n                placeholder={type === \"cash\" ? \"e.g. Wallet, Cash at home\" : \"e.g. Chase Savings\"}\n                autoFocus\n              />\n            </div>\n\n            <div className=\"grid grid-cols-2 gap-3\">\n              <div className=\"space-y-1.5\">\n                <Label>Institution</Label>\n                <Input\n                  value={institution}\n                  onChange={(e) => setInstitution(e.target.value)}\n                  placeholder=\"Optional\"\n                />\n              </div>\n              <div className=\"space-y-1.5\">\n                <Label>Type</Label>\n                <Select value={type} onValueChange={(v) => setType(v as Account[\"type\"])}>\n                  <SelectTrigger><SelectValue /></SelectTrigger>\n                  <SelectContent>\n                    {typeOptions.map((t) => (\n                      <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </div>\n            </div>\n\n            <div className=\"grid grid-cols-2 gap-3\">\n              <div className=\"space-y-1.5\">\n                <Label>{isDebtAccount(type) ? \"Balance owed\" : isIlliquidAsset(type) ? \"Current value\" : type === \"cash\" ? \"Amount of cash\" : \"Current balance\"}</Label>\n                <Input\n                  type=\"number\"\n                  step=\"0.01\"\n                  value={balance}\n                  onChange={(e) => setBalance(e.target.value)}\n                  placeholder=\"0.00\"\n                  onKeyDown={(e) => e.key === \"Enter\" && void submit()}\n                />\n              </div>\n              {type === \"credit\" ? (\n                <div className=\"space-y-1.5\">\n                  <Label>Credit limit</Label>\n                  <Input\n                    type=\"number\"\n                    step=\"0.01\"\n                    value={creditLimit}\n                    onChange={(e) => setCreditLimit(e.target.value)}\n                    placeholder=\"Optional\"\n                  />\n                </div>\n              ) : isIlliquidAsset(type) ? (\n                type === \"property\" && (\n                  <div className=\"space-y-1.5\">\n                    <Label>Property type</Label>\n                    <Select value={propertyType} onValueChange={(v) => setPropertyType(v as NonNullable<Account[\"propertyType\"]>)}>\n                      <SelectTrigger><SelectValue /></SelectTrigger>\n                      <SelectContent>\n                        {PROPERTY_TYPES.map((t) => (\n                          <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>\n                        ))}\n                      </SelectContent>\n                    </Select>\n                  </div>\n                )\n              ) : type === \"cash\" ? null : (\n                <div className=\"space-y-1.5\">\n                  <Label>Last 4 digits</Label>\n                  <Input\n                    value={mask}\n                    onChange={(e) => setMask(e.target.value.slice(0, 4))}\n                    placeholder=\"Optional\"\n                  />\n                </div>\n              )}\n            </div>\n\n            {isIlliquidAsset(type) && (\n              <div className=\"space-y-3 border-t border-border/40 pt-3\">\n                <p className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">Purchase details</p>\n                <div className=\"grid grid-cols-2 gap-3\">\n                  <div className=\"space-y-1.5\">\n                    <Label>Purchase price</Label>\n                    <Input\n                      type=\"number\" step=\"0.01\" value={purchasePrice}\n                      onChange={(e) => setPurchasePrice(e.target.value)}\n                      placeholder=\"Optional\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label>Purchase date</Label>\n                    <DatePicker\n                      value={purchaseDate || null}\n                      onChange={(d) => setPurchaseDate(d ?? \"\")}\n                      maxDate={new Date()}\n                      clearable\n                    />\n                  </div>\n                </div>\n              </div>\n            )}\n\n            {type === \"loan\" && (\n              <div className=\"space-y-3 border-t border-border/40 pt-3\">\n                <p className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">Loan terms</p>\n                <div className=\"grid grid-cols-2 gap-3\">\n                  <div className=\"space-y-1.5\">\n                    <Label>Original principal</Label>\n                    <Input\n                      type=\"number\" step=\"0.01\" value={originalPrincipal}\n                      onChange={(e) => setOriginalPrincipal(e.target.value)}\n                      placeholder=\"Optional\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label>Interest rate (%)</Label>\n                    <Input\n                      type=\"number\" step=\"0.01\" value={interestRate}\n                      onChange={(e) => setInterestRate(e.target.value)}\n                      placeholder=\"Optional\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label>Monthly payment</Label>\n                    <Input\n                      type=\"number\" step=\"0.01\" value={monthlyPayment}\n                      onChange={(e) => setMonthlyPayment(e.target.value)}\n                      placeholder=\"Optional\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label>Term (months)</Label>\n                    <Input\n                      type=\"number\" step=\"1\" value={termMonths}\n                      onChange={(e) => setTermMonths(e.target.value)}\n                      placeholder=\"Optional\"\n                    />\n                  </div>\n                </div>\n                <div className=\"space-y-1.5\">\n                  <Label>Origination date</Label>\n                  <DatePicker\n                    value={originationDate || null}\n                    onChange={(d) => setOriginationDate(d ?? \"\")}\n                    maxDate={new Date()}\n                    clearable\n                  />\n                </div>\n              </div>\n            )}\n\n            {isDebtAccount(type) && (\n              <p className=\"text-xs text-muted-foreground\">\n                Enter what you owe as a positive number, it's stored as a debt so\n                your net worth adds up.\n              </p>\n            )}\n          </div>\n        )}\n\n        <DialogFooter>\n          <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n            {plaidManaged ? \"Close\" : \"Cancel\"}\n          </Button>\n          {!plaidManaged && (\n            <Button onClick={() => void submit()} disabled={saving}>\n              {saving ? \"Saving…\" : editing ? \"Save\" : \"Add account\"}\n            </Button>\n          )}\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
    },
    {
      "name": "src/components/accounts/AccountNotes.tsx",
      "type": "text/plain",
      "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { Check, Loader2, NotebookPen, TriangleAlert } from \"lucide-react\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\n\nconst AUTOSAVE_MS = 700;\n\ntype Status = \"idle\" | \"saving\" | \"saved\" | \"error\";\n\n/**\n * Free-form notes on an asset or loan: who else is involved, what was agreed,\n * where the paperwork is. There is no Save button: the text is written a moment\n * after you stop typing, when you click away, and when you leave the page.\n *\n * Give it `key={accountId}` so switching accounts starts from that account's\n * notes instead of carrying the previous text over.\n */\nexport function AccountNotes({\n  value,\n  onSave,\n  className,\n}: {\n  value: string | null | undefined;\n  onSave: (notes: string) => Promise<void> | void;\n  className?: string;\n}) {\n  const [text, setText] = useState(value ?? \"\");\n  const [status, setStatus] = useState<Status>(\"idle\");\n  const saved = useRef(value ?? \"\");\n  const latest = useRef(text);\n  const onSaveRef = useRef(onSave);\n  onSaveRef.current = onSave;\n\n  const save = useCallback(async (next: string) => {\n    if (next === saved.current) return;\n    setStatus(\"saving\");\n    try {\n      await onSaveRef.current(next);\n      saved.current = next;\n      // Typing may have moved on while this was in flight; that has its own save queued.\n      setStatus(latest.current === next ? \"saved\" : \"saving\");\n    } catch {\n      setStatus(\"error\");\n    }\n  }, []);\n\n  // Write shortly after typing stops.\n  useEffect(() => {\n    latest.current = text;\n    if (text === saved.current) return;\n    const timer = setTimeout(() => void save(text), AUTOSAVE_MS);\n    return () => clearTimeout(timer);\n  }, [text, save]);\n\n  // Leaving the page with unsaved typing still saves it.\n  useEffect(\n    () => () => {\n      if (latest.current !== saved.current) void onSaveRef.current(latest.current);\n    },\n    []\n  );\n\n  return (\n    <Card className={cn(\"border-border/60 flex flex-col\", className)}>\n      <CardHeader className=\"pb-2 flex-row items-center justify-between space-y-0\">\n        <CardTitle className=\"text-sm font-medium text-muted-foreground flex items-center gap-2\">\n          <NotebookPen size={14} strokeWidth={1.5} /> Notes\n        </CardTitle>\n        <span\n          role=\"status\"\n          aria-live=\"polite\"\n          className={cn(\n            \"flex items-center gap-1 text-[11px] transition-opacity\",\n            status === \"idle\" ? \"opacity-0\" : \"opacity-100\",\n            status === \"error\" ? \"text-chart-red\" : \"text-muted-foreground\"\n          )}\n        >\n          {status === \"saving\" && (<><Loader2 size={11} className=\"animate-spin\" /> Saving…</>)}\n          {status === \"saved\" && (<><Check size={11} /> Saved</>)}\n          {status === \"error\" && (<><TriangleAlert size={11} /> Couldn't save, still trying as you type</>)}\n        </span>\n      </CardHeader>\n      <CardContent className=\"flex-1 flex flex-col pt-0\">\n        <Textarea\n          value={text}\n          onChange={(e) => setText(e.target.value)}\n          onBlur={() => void save(text)}\n          placeholder=\"Anything worth remembering: who else is involved, what was agreed, where the paperwork is…\"\n          aria-label=\"Notes\"\n          className=\"min-h-[140px] flex-1 resize-y bg-background text-sm leading-relaxed\"\n        />\n        <p className=\"mt-2 text-[11px] text-muted-foreground\">Saves automatically as you type.</p>\n      </CardContent>\n    </Card>\n  );\n}\n"
    },
    {
      "name": "src/components/accounts/AccountTransactions.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useMemo, useState } from \"react\";\nimport { format } from \"date-fns\";\nimport { ArrowDown, ArrowUp, ArrowUpDown, Plus, SearchX, X } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from \"@/components/ui/pagination\";\nimport { CategoryBadge, MerchantAvatar } from \"@/components/elements/CategoryIcon\";\nimport { CategoryPicker } from \"@/components/elements/CategoryPicker\";\nimport { TagFilterPicker } from \"@/components/elements/TagFilterPicker\";\nimport { SearchInput } from \"@/components/elements/SearchInput\";\nimport { EmptyState } from \"@/components/elements/EmptyState\";\nimport { TransactionDetail } from \"@/components/TransactionDetail\";\nimport { TransferBadge } from \"@/components/transactions/TransferBadge\";\nimport { DuplicateBadge } from \"@/components/transactions/DuplicateBadge\";\nimport { SplitBadge } from \"@/components/transactions/SplitBadge\";\nimport { AddTransactionDialog } from \"@/components/transactions/AddTransactionDialog\";\nimport { StatCard } from \"@/components/dashboard/StatCard\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { useElementHeight } from \"@/hooks/useElementHeight\";\nimport { useTransactionSearch } from \"@/hooks/useTransactionSearch\";\nimport { useMerchantRefinements } from \"@/hooks/useMerchantRefinements\";\nimport { useSuggestedTransfers, useTransferAction } from \"@/hooks/useTransfers\";\nimport { useSuggestedDuplicates, useDuplicateAction } from \"@/hooks/useDuplicates\";\nimport { useTags } from \"@/hooks/useTags\";\nimport { dayLabel, withDayHeaders, type DayHeaderRow } from \"@/lib/dayGroups\";\nimport { refinedMerchantLabel } from \"@/lib/merchant\";\nimport { isAssetAccount, canHoldTransactions } from \"@/lib/accounts\";\nimport { CATEGORIES } from \"@/lib/categories\";\nimport { cn, parseLocalDate } from \"@/lib/utils\";\nimport { toast } from \"sonner\";\nimport type { Account, Transaction } from \"@/types/finance\";\n\n/** Same choices, default and remembered value as the main Transactions table. */\nconst PAGE_SIZES = [10, 20, 50, 100] as const;\nconst DEFAULT_PAGE_SIZE = 20;\nconst PAGE_SIZE_KEY = \"txn-page-size\";\n/** Height of the app's sticky top bar (Layout's h-12); the filter bar sticks below it. */\nconst APP_HEADER = 48;\n\ntype SortKey = \"date\" | \"description\" | \"amount\";\n\nconst isDayHeader = (r: Transaction | DayHeaderRow): r is DayHeaderRow => \"isDayHeader\" in r;\n\nfunction loadPageSize(): number {\n  try {\n    const n = Number(localStorage.getItem(PAGE_SIZE_KEY));\n    return (PAGE_SIZES as readonly number[]).includes(n) ? n : DEFAULT_PAGE_SIZE;\n  } catch {\n    return DEFAULT_PAGE_SIZE;\n  }\n}\n\n/** Which tags an asset last showed, so opening the Honda page again is still the Honda view. */\nconst tagMemoryKey = (accountId: string) => `account-tags:${accountId}`;\nfunction loadTags(accountId: string): string[] {\n  try {\n    const raw = localStorage.getItem(tagMemoryKey(accountId));\n    const parsed = raw ? JSON.parse(raw) : [];\n    return Array.isArray(parsed) ? parsed.filter((v) => typeof v === \"string\") : [];\n  } catch {\n    return [];\n  }\n}\n\nconst fmtDate = (d: string) => {\n  try { return format(parseLocalDate(d), \"MMM d, yyyy\"); } catch { return d; }\n};\n\n/**\n * The transaction table for one account, built like the Transactions page:\n * a filter bar, a column header and per-day headers that all stick as you\n * scroll, server-side paging with a rows-per-page choice, and a detail panel\n * that opens right here instead of navigating away.\n *\n * Cars, homes, loans and cash rarely have transactions of their own; what\n * belongs to them (insurance, repairs, repayments) sits in your bank accounts.\n * So on those accounts, picking a tag shows everything carrying that tag from\n * every account, with a switch back to only this account's own rows.\n */\nexport function AccountTransactions({ account }: { account: Account }) {\n  const { updateTransactions, addTransaction } = useFinanceData();\n  const { moneyAbs: fmtMoney } = useMoney(account.currency);\n  const { data: refinements } = useMerchantRefinements();\n  const { data: tags = [] } = useTags();\n  const { byTxnId: suggestedTransfers } = useSuggestedTransfers();\n  const { byTxnId: suggestedDuplicates } = useSuggestedDuplicates();\n\n  const assetLike = isAssetAccount(account.type);\n\n  const [search, setSearch] = useState(\"\");\n  const [category, setCategory] = useState(\"all\");\n  const [type, setType] = useState(\"all\");\n  const [tagIds, setTagIdsState] = useState<string[]>(() => loadTags(account.id));\n  const [tagScope, setTagScope] = useState<\"all\" | \"account\">(\"all\");\n  const [sortKey, setSortKey] = useState<SortKey>(\"date\");\n  const [sortDir, setSortDir] = useState<\"asc\" | \"desc\">(\"desc\");\n  const [page, setPage] = useState(1);\n  const [pageSize, setPageSizeState] = useState(loadPageSize);\n  const [detailTxn, setDetailTxn] = useState<Transaction | null>(null);\n  const [addOpen, setAddOpen] = useState(false);\n\n  const [filterBarRef, filterBarHeight] = useElementHeight<HTMLDivElement>();\n  const [columnHeaderRef, columnHeaderHeight] = useElementHeight<HTMLDivElement>();\n\n  // A tag that has since been deleted must not leave the table empty behind an invisible filter.\n  useEffect(() => {\n    if (tagIds.length > 0 && tags.length > 0) {\n      const stillExists = tagIds.filter((id) => tags.some((t) => t.id === id));\n      if (stillExists.length !== tagIds.length) setTagIdsState(stillExists);\n    }\n  }, [tags, tagIds]);\n\n  const setTags = (ids: string[]) => {\n    setTagIdsState(ids);\n    setPage(1);\n    try { localStorage.setItem(tagMemoryKey(account.id), JSON.stringify(ids)); } catch { /* not remembered in private mode */ }\n  };\n  const setPageSize = (n: number) => {\n    setPageSizeState(n);\n    setPage(1);\n    try { localStorage.setItem(PAGE_SIZE_KEY, String(n)); } catch { /* ditto */ }\n  };\n\n  // Asset-like accounts follow a tag across every account; bank accounts keep the account.\n  const acrossAccounts = assetLike && tagIds.length > 0 && tagScope === \"all\";\n  const filters = {\n    search,\n    category,\n    type,\n    tagIds,\n    accountId: acrossAccounts ? \"all\" : account.id,\n    sort: sortKey,\n    direction: sortDir,\n  };\n  const { rows, filteredCount, income, expenses, loading, error, refresh } = useTransactionSearch(filters, { pageSize, page });\n  const { confirm: confirmTransfer, reject: rejectTransfer } = useTransferAction(refresh);\n  const { merge: mergeDuplicate, dismiss: dismissDuplicate } = useDuplicateAction(refresh);\n\n  const activeTags = tags.filter((t) => tagIds.includes(t.id));\n  const filtersActive = search !== \"\" || category !== \"all\" || type !== \"all\" || tagIds.length > 0;\n  const clearFilters = () => { setSearch(\"\"); setCategory(\"all\"); setType(\"all\"); setTags([]); setPage(1); };\n\n  const totalPages = Math.max(1, Math.ceil(filteredCount / pageSize));\n  useEffect(() => { if (page > totalPages) setPage(totalPages); }, [page, totalPages]);\n  const rangeStart = filteredCount === 0 ? 0 : (page - 1) * pageSize + 1;\n  const rangeEnd = Math.min(page * pageSize, filteredCount);\n\n  const display = useMemo(() => (sortKey === \"date\" ? withDayHeaders(rows) : rows), [rows, sortKey]);\n\n  const toggleSort = (key: SortKey) => {\n    if (sortKey === key) setSortDir(sortDir === \"asc\" ? \"desc\" : \"asc\");\n    else { setSortKey(key); setSortDir(key === \"description\" ? \"asc\" : \"desc\"); }\n    setPage(1);\n  };\n  const SortIcon = ({ k }: { k: SortKey }) =>\n    sortKey !== k ? <ArrowUpDown size={11} strokeWidth={1.5} className=\"opacity-40\" />\n      : sortDir === \"asc\" ? <ArrowUp size={11} strokeWidth={1.5} /> : <ArrowDown size={11} strokeWidth={1.5} />;\n\n  const handleCategoryChange = (txnId: string, newCategory: string) => {\n    updateTransactions([{ id: txnId, category: newCategory }], { applyToMerchant: true })\n      .then((n) => {\n        void refresh();\n        const others = typeof n === \"number\" ? n : 0;\n        toast.success(others > 0 ? `Moved to ${newCategory}, and ${others} more from this merchant` : `Moved to ${newCategory}`);\n      })\n      .catch(() => toast.error(\"Couldn't update the category\"));\n    setDetailTxn((d) => (d && d.id === txnId ? { ...d, category: newCategory } : d));\n  };\n\n  const columns = \"grid-cols-[1fr_auto] sm:grid-cols-[minmax(0,1fr)_120px_130px_120px]\";\n  const activeTagNames = activeTags.map((t) => t.name);\n  const activeTagLabel =\n    activeTagNames.length <= 2\n      ? activeTagNames.join(\" and \")\n      : `${activeTagNames.slice(0, -1).join(\", \")} and ${activeTagNames[activeTagNames.length - 1]}`;\n\n  return (\n    <section className=\"space-y-3\" aria-label=\"Transactions\">\n      <div className=\"flex flex-wrap items-end justify-between gap-3 px-1\">\n        <div>\n          <h2 className=\"text-xs uppercase tracking-wider font-semibold text-muted-foreground/60\">Transactions</h2>\n          <p className=\"text-sm text-muted-foreground mt-0.5\">\n            {acrossAccounts && activeTags.length > 0\n              ? <>Everything tagged <span className=\"font-medium text-foreground\">{activeTagLabel}</span>, from all your accounts</>\n              : \"Activity recorded on this account\"}\n          </p>\n        </div>\n        {canHoldTransactions(account.type) && (\n          <Button size=\"sm\" className=\"gap-1.5\" onClick={() => setAddOpen(true)}>\n            <Plus size={14} strokeWidth={1.5} /> Add transaction\n          </Button>\n        )}\n      </div>\n\n      {/* Totals for whatever's currently filtered (tags included) - not just\n          this account's lifetime totals, so narrowing by tag shows exactly\n          what that slice adds up to. */}\n      {filteredCount > 0 && (\n        <div className=\"grid grid-cols-2 gap-3 px-1\">\n          <StatCard title=\"Total Credits\" value={`+${fmtMoney(income)}`} />\n          <StatCard title=\"Total Debits\" value={`-${fmtMoney(expenses)}`} />\n        </div>\n      )}\n\n      {/* Sticks under the app header; the column header and day rows stack below it. */}\n      <div\n        ref={filterBarRef}\n        style={{ top: APP_HEADER }}\n        className=\"sticky z-20 bg-background -mx-2 px-2 pt-2 pb-3 border-b border-border/30 space-y-2\"\n      >\n        <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2\">\n          <SearchInput\n            value={search}\n            onChange={(v) => { setSearch(v); setPage(1); }}\n            placeholder=\"Search this table…\"\n            shortcut={false}\n          />\n          <CategoryPicker\n            value={category}\n            onChange={(v) => { setCategory(v); setPage(1); }}\n            categories={CATEGORIES}\n            allLabel=\"All Categories\"\n            aria-label=\"Filter by category\"\n          />\n          <Select value={type} onValueChange={(v) => { setType(v); setPage(1); }}>\n            <SelectTrigger className=\"bg-background\" aria-label=\"Filter by type\"><SelectValue placeholder=\"Type\" /></SelectTrigger>\n            <SelectContent>\n              <SelectItem value=\"all\">All Types</SelectItem>\n              <SelectItem value=\"income\">Income</SelectItem>\n              <SelectItem value=\"expense\">Expense</SelectItem>\n            </SelectContent>\n          </Select>\n          <TagFilterPicker tags={tags} selectedIds={tagIds} onChange={setTags} />\n        </div>\n\n        {(filtersActive || (assetLike && tagIds.length > 0)) && (\n          <div className=\"flex flex-wrap items-center gap-2 text-xs\">\n            {assetLike && tagIds.length > 0 && (\n              <div role=\"group\" aria-label=\"Tag scope\" className=\"inline-flex rounded-md border border-border/60 p-0.5 bg-muted/40\">\n                {([[\"all\", \"All accounts\"], [\"account\", \"This account only\"]] as const).map(([v, label]) => (\n                  <button\n                    key={v}\n                    onClick={() => { setTagScope(v); setPage(1); }}\n                    aria-pressed={tagScope === v}\n                    className={cn(\n                      \"px-2.5 py-1 rounded-[5px] transition-colors\",\n                      tagScope === v ? \"bg-background text-foreground shadow-sm font-medium\" : \"text-muted-foreground hover:text-foreground\"\n                    )}\n                  >\n                    {label}\n                  </button>\n                ))}\n              </div>\n            )}\n            {filtersActive && (\n              <Button variant=\"ghost\" size=\"sm\" className=\"h-7 gap-1 text-muted-foreground\" onClick={clearFilters}>\n                <X size={12} strokeWidth={1.5} /> Clear filters\n              </Button>\n            )}\n          </div>\n        )}\n      </div>\n\n      <Card className=\"border-border/50\">\n        <CardContent className=\"p-0\">\n          <div\n            ref={columnHeaderRef}\n            style={{ top: APP_HEADER + filterBarHeight }}\n            className={cn(\"sticky z-10 bg-card grid gap-2 px-5 py-2.5 border-b border-border/30 text-xs text-muted-foreground items-center\", columns)}\n          >\n            <button className=\"flex items-center gap-1 text-left hover:text-foreground w-fit\" onClick={() => toggleSort(\"description\")}>\n              Merchant <SortIcon k=\"description\" />\n            </button>\n            <button className=\"hidden sm:flex items-center gap-1 hover:text-foreground w-fit\" onClick={() => toggleSort(\"date\")}>\n              Date <SortIcon k=\"date\" />\n            </button>\n            <span className=\"hidden sm:block\">Category</span>\n            <button className=\"flex items-center gap-1 justify-end hover:text-foreground ml-auto\" onClick={() => toggleSort(\"amount\")}>\n              Amount <SortIcon k=\"amount\" />\n            </button>\n          </div>\n\n          {error && <p className=\"text-sm text-chart-red text-center py-10\">Couldn't load transactions. Try again in a moment.</p>}\n\n          {!error && !loading && display.length === 0 && (\n            <EmptyState\n              icon={SearchX}\n              title={filtersActive ? \"No transactions match\" : \"No transactions yet\"}\n              description={\n                filtersActive\n                  ? \"Try removing a filter.\"\n                  : assetLike\n                    ? \"Add one here, or tag transactions from your bank accounts and pick that tag above to see them together.\"\n                    : \"Nothing has been recorded on this account yet.\"\n              }\n              className=\"py-14\"\n            />\n          )}\n\n          {display.map((row) => {\n            if (isDayHeader(row)) {\n              return (\n                <div\n                  key={row.id}\n                  style={{ top: APP_HEADER + filterBarHeight + columnHeaderHeight }}\n                  className=\"sticky z-[5] flex items-center justify-between px-5 py-1.5 bg-muted border-b border-border/30 text-xs text-muted-foreground\"\n                >\n                  <span className=\"font-medium\">{dayLabel(row.date)}</span>\n                  <span className={cn(\"tabular-nums\", row.net > 0 && \"text-chart-green\")}>\n                    {row.net > 0 ? \"+\" : row.net < 0 ? \"-\" : \"\"}{fmtMoney(row.net)}\n                  </span>\n                </div>\n              );\n            }\n            const txn = row;\n            return (\n              <div\n                key={txn.id}\n                role=\"button\"\n                tabIndex={0}\n                onClick={() => setDetailTxn(txn)}\n                onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); setDetailTxn(txn); } }}\n                className={cn(\n                  \"grid gap-2 px-5 py-3 border-b border-border/20 last:border-0 items-center cursor-pointer transition-colors\",\n                  \"hover:bg-muted/30 focus-visible:outline-none focus-visible:bg-muted/40 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-primary/40\",\n                  detailTxn?.id === txn.id && \"bg-muted/40\",\n                  columns\n                )}\n              >\n                <div className=\"flex items-center gap-3 min-w-0\">\n                  <MerchantAvatar description={txn.description} category={txn.category} />\n                  <div className=\"min-w-0\">\n                    <p className=\"text-sm font-medium text-foreground truncate\" title={txn.description}>\n                      {refinedMerchantLabel(txn.description, refinements, txn.date, txn.category)}\n                    </p>\n                    <p className=\"text-xs text-muted-foreground sm:hidden\">{fmtDate(txn.date)}</p>\n                  </div>\n                </div>\n                <span className=\"hidden sm:block text-sm text-muted-foreground\">{fmtDate(txn.date)}</span>\n                <span className=\"hidden sm:flex items-center min-w-0\" onClick={(e) => e.stopPropagation()}>\n                  {suggestedDuplicates.has(txn.id) ? (\n                    <DuplicateBadge\n                      suggested={suggestedDuplicates.get(txn.id)!}\n                      onMerge={() => mergeDuplicate.mutate(suggestedDuplicates.get(txn.id)!.id)}\n                      onDismiss={() => dismissDuplicate.mutate(suggestedDuplicates.get(txn.id)!.id)}\n                      busy={mergeDuplicate.isPending || dismissDuplicate.isPending}\n                    />\n                  ) : txn.isTransfer ? (\n                    <TransferBadge\n                      suggested={suggestedTransfers.get(txn.id)}\n                      onConfirm={() => confirmTransfer.mutate(suggestedTransfers.get(txn.id)!.id)}\n                      onReject={() => rejectTransfer.mutate(suggestedTransfers.get(txn.id)!.id)}\n                      busy={confirmTransfer.isPending || rejectTransfer.isPending}\n                    />\n                  ) : txn.isSplit ? (\n                    <SplitBadge count={txn.splitCount ?? 0} />\n                  ) : (\n                    <CategoryBadge category={txn.category} />\n                  )}\n                </span>\n                <span className={cn(\"text-sm font-medium text-right tabular-nums\", txn.amount >= 0 ? \"text-chart-green\" : \"text-foreground\")}>\n                  {txn.amount >= 0 ? \"+\" : \"-\"}{fmtMoney(txn.amount)}\n                </span>\n              </div>\n            );\n          })}\n\n          {filteredCount > 0 && (\n            <div className=\"flex flex-wrap items-center justify-between gap-x-6 gap-y-2 border-t border-border/30 px-5 py-3\">\n              <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n                <span>Rows per page</span>\n                <Select value={String(pageSize)} onValueChange={(v) => setPageSize(Number(v))}>\n                  <SelectTrigger className=\"h-8 w-[72px] text-xs\" aria-label=\"Rows per page\"><SelectValue /></SelectTrigger>\n                  <SelectContent>\n                    {PAGE_SIZES.map((n) => <SelectItem key={n} value={String(n)}>{n}</SelectItem>)}\n                  </SelectContent>\n                </Select>\n                <span className=\"tabular-nums ml-2\">{rangeStart}–{rangeEnd} of {filteredCount}</span>\n              </div>\n              {totalPages > 1 && (\n                <Pagination className=\"mx-0 w-auto\">\n                  <PaginationContent>\n                    <PaginationItem><PaginationPrevious onClick={() => setPage(Math.max(1, page - 1))} className={cn(\"cursor-pointer\", page === 1 && \"pointer-events-none opacity-50\")} /></PaginationItem>\n                    {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {\n                      const start = Math.max(1, Math.min(page - 2, totalPages - 4));\n                      const p = start + i;\n                      return (\n                        <PaginationItem key={p}>\n                          <PaginationLink className=\"cursor-pointer\" isActive={p === page} onClick={() => setPage(p)}>{p}</PaginationLink>\n                        </PaginationItem>\n                      );\n                    })}\n                    <PaginationItem><PaginationNext onClick={() => setPage(Math.min(totalPages, page + 1))} className={cn(\"cursor-pointer\", page === totalPages && \"pointer-events-none opacity-50\")} /></PaginationItem>\n                  </PaginationContent>\n                </Pagination>\n              )}\n            </div>\n          )}\n        </CardContent>\n      </Card>\n\n      {/* Opens on this page: clicking a row never leaves the account. */}\n      <TransactionDetail\n        transaction={detailTxn}\n        open={!!detailTxn}\n        onOpenChange={(o) => { if (!o) setDetailTxn(null); }}\n        onCategoryChange={handleCategoryChange}\n        onNoteChange={async (txnId, note) => {\n          try {\n            await updateTransactions([{ id: txnId, note }]);\n            void refresh();\n            setDetailTxn((d) => (d && d.id === txnId ? { ...d, note } : d));\n          } catch {\n            toast.error(\"Couldn't save the note\");\n          }\n        }}\n        onSplitsChange={() => void refresh()}\n        categories={[...CATEGORIES]}\n      />\n\n      <AddTransactionDialog\n        open={addOpen}\n        onOpenChange={setAddOpen}\n        accounts={[account]}\n        onAdd={async (t) => {\n          await addTransaction(t);\n          void refresh();\n        }}\n      />\n    </section>\n  );\n}\n"
    },
    {
      "name": "src/components/accounts/AccountsView.tsx",
      "type": "text/plain",
      "content": "import { BankReconnectButton } from \"@/components/plaid/BankReconnectButton\";\nimport { ConnectBankButton } from \"@/components/plaid/ConnectBankButton\";\nimport { formatMoney } from \"@/lib/format\";\nimport { useState, useMemo } from \"react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from \"@/components/ui/dialog\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { cn } from \"@/lib/utils\";\nimport { api } from \"@/lib/api\";\nimport { Plus, Landmark, CreditCard, PiggyBank, TrendingUp, Banknote, RefreshCw, Building2, Pencil, Car, Home, Wallet, NotebookPen } from \"lucide-react\";\nimport { useNavigate, Link } from \"react-router-dom\";\nimport { toast } from \"sonner\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport { useDocumentTitle } from \"@/hooks/useDocumentTitle\";\nimport type { Account } from \"@/types/finance\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { AnimatedMoney } from \"@/components/elements/AnimatedMoney\";\nimport { AccountDialog } from \"@/components/accounts/AccountDialog\";\nimport { PageSkeleton } from \"@/components/skeletons/LoadingSkeletons\";\nimport { isDebtAccount, isBankAccount, isAssetAccount, accountDisplayName, accountSubtitle, isIlliquidAsset } from \"@/lib/accounts\";\nimport { ACCOUNT_TINTS } from \"@/components/accounts/accountTints\";\nimport { WeightBar } from \"@/components/dashboard/WeightBar\";\nimport { shareColor } from \"@/components/dashboard/shareColors\";\nimport { Progress } from \"@/components/ui/progress\";\nimport {\n  AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,\n  AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\";\n\nconst typeIcons: Record<string, typeof Landmark> = {\n  checking: Landmark,\n  savings: PiggyBank,\n  credit: CreditCard,\n  investment: TrendingUp,\n  loan: Banknote,\n  vehicle: Car,\n  property: Home,\n  cash: Wallet,\n};\n\nconst typeBorderColors: Record<string, string> = {\n  checking: \"border-l-chart-blue\",\n  savings: \"border-l-chart-green\",\n  credit: \"border-l-chart-orange\",\n  investment: \"border-l-chart-purple\",\n  loan: \"border-l-chart-red\",\n  vehicle: \"border-l-chart-yellow\",\n  property: \"border-l-chart-yellow\",\n  cash: \"border-l-chart-green\",\n};\n\n\nexport type AccountsScope = \"bank\" | \"assets\";\n\nconst BANK_TYPES: Account[\"type\"][] = [\"checking\", \"savings\", \"credit\"];\nconst ASSET_TYPES: Account[\"type\"][] = [\"cash\", \"investment\", \"property\", \"vehicle\", \"loan\"];\n\n/**\n * The account list, shared by two pages: Accounts (checking, savings, credit\n * cards: the everyday money) and Assets (cash on hand, investments, property,\n * vehicles and loans: what you own and owe beyond that).\n */\nexport const AccountsView = ({ scope }: { scope: AccountsScope }) => {\n  const isBank = scope === \"bank\";\n  const pageTitle = isBank ? \"Accounts\" : \"Assets\";\n  const scopeTypes = isBank ? BANK_TYPES : ASSET_TYPES;\n  const navigate = useNavigate();\n  const {\n    accounts, summaryAccounts, hasData, isLoading, plaidItems, syncPlaid, refreshData,\n    addAccount, updateAccount, deleteAccount,\n  } = useFinanceData();\n  const { moneyAbs: fmtCurrency } = useMoney();\n  const [editAccount, setEditAccount] = useState<Account | null>(null);\n  const [nickInput, setNickInput] = useState(\"\");\n  const [savingNick, setSavingNick] = useState(false);\n  const [editorOpen, setEditorOpen] = useState(false);\n  const [editorAccount, setEditorAccount] = useState<Account | null>(null);\n  const [confirmDelete, setConfirmDelete] = useState<Account | null>(null);\n  useDocumentTitle(pageTitle);\n\n  // Real sync time comes from the Plaid item (a true timestamp updated every\n  // sync), keyed by `plaid-<account_id>`.\n  const syncedAtById = useMemo(() => {\n    const m: Record<string, string> = {};\n    plaidItems.forEach((item) => {\n      if (!item?.last_synced) return;\n      (item.accounts || []).forEach((a: { account_id: string }) => {\n        m[`plaid-${a.account_id}`] = item.last_synced;\n      });\n    });\n    return m;\n  }, [plaidItems]);\n\n  const scoped = useMemo(\n    () => accounts.filter((a) => (isBank ? isBankAccount(a.type) : isAssetAccount(a.type))),\n    [accounts, isBank]\n  );\n  const ofType = (...types: Account[\"type\"][]) => scoped.filter((a) => types.includes(a.type));\n  const sections = isBank\n    ? [\n        { title: \"Bank accounts\", list: ofType(\"checking\", \"savings\") },\n        { title: \"Credit cards\", list: ofType(\"credit\") },\n      ]\n    : [\n        { title: \"Cash\", list: ofType(\"cash\") },\n        { title: \"Investments\", list: ofType(\"investment\") },\n        { title: \"Property & vehicles\", list: ofType(\"property\", \"vehicle\") },\n        { title: \"Loans\", list: ofType(\"loan\") },\n      ];\n  const sum = (list: Account[]) => list.filter(a => summaryAccounts.some(s => s.id === a.id)).reduce((t, a) => t + a.balance, 0);\n  const owedTotal = ofType(\"loan\").filter(a => summaryAccounts.some(s => s.id === a.id)).reduce((t, a) => t + Math.abs(a.balance), 0);\n  const ownedTotal = sum(ofType(\"cash\", \"investment\", \"property\", \"vehicle\"));\n\n  const syncLabel = (account: Account) => {\n    const iso = syncedAtById[account.id] || account.lastSynced;\n    const d = iso ? new Date(iso) : null;\n    const item = plaidItems.find(item => item.accounts?.some(a => `plaid-${a.account_id}` === account.id));\n    const stamp = d && !isNaN(d.getTime()) ? `Synced ${formatDistanceToNow(d, { addSuffix: true })}` : \"Not synced yet\";\n    return item?.requires_reconnect ? `${stamp} · Sign-in required` : item?.sync_error ? `${stamp} · Sync needs attention` : stamp;\n  };\n\n  /** Only Plaid-backed accounts have anything to sync. */\n  const isPlaid = (a: Account) => a.id.startsWith(\"plaid-\");\n\n  const openRename = (account: Account) => {\n    setEditAccount(account);\n    setNickInput(account.nickname?.trim() || account.name);\n  };\n\n  const saveNickname = async () => {\n    if (!editAccount) return;\n    setSavingNick(true);\n    try {\n      // Empty input clears the nickname (falls back to the bank-given name).\n      const next = nickInput.trim() === editAccount.name ? \"\" : nickInput.trim();\n      await api.put<Account>(\"accounts\", editAccount.id, { nickname: next });\n      refreshData();\n      toast.success(\"Account name updated\");\n      setEditAccount(null);\n    } catch {\n      toast.error(\"Couldn't update the name\");\n    } finally {\n      setSavingNick(false);\n    }\n  };\n\n  if (isLoading) {\n    return (\n      <Layout>\n        <PageSkeleton />\n      </Layout>\n    );\n  }\n\n  if (!hasData || scoped.length === 0) {\n    return (\n      <Layout>\n        <div className=\"space-y-6\">\n          <div>\n            <h1 className=\"text-2xl font-bold text-foreground tracking-tight\">{pageTitle}</h1>\n            <p className=\"text-sm text-muted-foreground mt-1\">\n              {isBank ? \"Manage your connected accounts\" : \"Cash, investments, property, vehicles and loans\"}\n            </p>\n          </div>\n          <div className=\"flex flex-col items-center justify-center py-16 gap-6\">\n            <div className=\"h-24 w-24 rounded-full bg-gradient-to-tr from-primary/10 to-primary/30 flex items-center justify-center border border-primary/20 shadow-inner\">\n              <Building2 size={38} strokeWidth={1.5} className=\"text-primary/70\" />\n            </div>\n            <div className=\"text-center space-y-2 max-w-md\">\n              <h2 className=\"text-lg font-semibold text-foreground\">{isBank ? \"No accounts connected\" : \"No assets or loans yet\"}</h2>\n              <p className=\"text-sm text-muted-foreground\">\n                {isBank\n                  ? \"Connect your bank securely to see your balances and transactions here.\"\n                  : \"Add an investment account, a car, a home or a loan to count it toward your net worth.\"}\n              </p>\n            </div>\n            <div className=\"flex flex-wrap justify-center gap-3\">\n              {isBank ? (\n                <ConnectBankButton />\n              ) : (\n                <Link to=\"/investments\">\n                  <Button variant=\"outline\" className=\"gap-2\">\n                    <TrendingUp size={16} strokeWidth={1.5} /> Brokerage portfolios\n                  </Button>\n                </Link>\n              )}\n              {!isBank && <Button className=\"gap-2\" onClick={() => { setEditorAccount(null); setEditorOpen(true); }}>\n                <Plus size={16} strokeWidth={1.5} /> Add asset, cash or loan\n              </Button>}\n            </div>\n          </div>\n        </div>\n\n      <AccountDialog\n        open={editorOpen}\n        onOpenChange={(o) => { setEditorOpen(o); if (!o) setEditorAccount(null); }}\n        account={editorAccount}\n        types={scopeTypes}\n        onAdd={addAccount}\n        onUpdate={updateAccount}\n      />\n\n      <AlertDialog open={!!confirmDelete} onOpenChange={(o) => !o && setConfirmDelete(null)}>\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>Remove \"{confirmDelete && accountDisplayName(confirmDelete)}\"?</AlertDialogTitle>\n            <AlertDialogDescription>\n              The account is removed from your list. Transactions recorded against\n              it stay in your history, so your totals do not change.\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>Cancel</AlertDialogCancel>\n            <AlertDialogAction\n              className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n              onClick={async () => {\n                const target = confirmDelete;\n                setConfirmDelete(null);\n                if (!target) return;\n                try {\n                  await deleteAccount(target.id);\n                  toast.success(`${accountDisplayName(target)} removed`);\n                } catch {\n                  toast.error(\"Couldn't remove the account\");\n                }\n              }}\n            >\n              Remove\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n      </Layout>\n    );\n  }\n\n  const renderCard = (account: Account) => {\n    const fmtCurrency = (n: number) => formatMoney(Math.abs(n), account.currency || \"USD\");\n    const Icon = typeIcons[account.type] || Landmark;\n    const borderColor = typeBorderColors[account.type] || \"border-l-primary\";\n    const isDebt = isDebtAccount(account.type);\n    const owed = Math.abs(Math.min(account.balance, 0));\n    const availableCredit = account.available ?? null;\n    const plaidItem = plaidItems.find(item => item.accounts?.some(a => `plaid-${a.account_id}` === account.id));\n\n    return (\n      <Card\n        key={account.id}\n        className={cn(\"bg-gradient-to-br from-card to-card/50 border-border/60 border-l-4 cursor-pointer hover:bg-muted/30 transition-colors shadow-sm overflow-hidden relative group\", borderColor)}\n        onClick={() => navigate(`/accounts/${account.id}`)}\n      >\n        <div className=\"absolute inset-x-0 bottom-0 h-1 bg-gradient-to-r from-primary/50 to-primary opacity-0 group-hover:opacity-100 transition-opacity\" />\n        <CardContent className=\"p-5\">\n          <div className=\"flex items-start justify-between\">\n            <div className=\"flex items-center gap-3 min-w-0\">\n              <div className=\"h-10 w-10 rounded-full bg-muted flex items-center justify-center shrink-0\">\n                <Icon size={18} strokeWidth={1.5} className=\"text-muted-foreground\" />\n              </div>\n              <div className=\"min-w-0\">\n                <div className=\"flex items-center gap-1.5\">\n                  <p className=\"text-sm font-semibold text-foreground truncate\">{accountDisplayName(account)}</p>\n                  <button\n                    className=\"opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-primary shrink-0\"\n                    aria-label=\"Rename account\"\n                    onClick={(e) => { e.stopPropagation(); openRename(account); }}\n                  >\n                    <Pencil size={12} strokeWidth={1.5} />\n                  </button>\n                </div>\n                <p className=\"text-xs text-muted-foreground truncate\">{accountSubtitle(account)}</p>\n              </div>\n            </div>\n            <div className=\"text-right shrink-0 ml-2\">\n              {isDebt ? (\n                availableCredit !== null ? (\n                  <>\n                    <p className=\"text-lg font-bold tabular-nums text-foreground\"><AnimatedMoney currency={account.currency} value={availableCredit} signDisplay=\"never\" /></p>\n                    <p className=\"text-xs text-muted-foreground\">Available credit</p>\n                  </>\n                ) : (\n                  <>\n                    <p className=\"text-lg font-bold tabular-nums text-foreground\"><AnimatedMoney currency={account.currency} value={owed} signDisplay=\"never\" /></p>\n                    <p className=\"text-xs text-muted-foreground\">Balance owed</p>\n                  </>\n                )\n              ) : (\n                <>\n                  <p className={cn(\"text-lg font-bold tabular-nums\", account.balance >= 0 ? \"text-foreground\" : \"text-chart-red\")}>\n                    {account.balance < 0 ? \"-\" : \"\"}<AnimatedMoney currency={account.currency} value={Math.abs(account.balance)} signDisplay=\"never\" />\n                  </p>\n                  <p className=\"text-xs text-muted-foreground capitalize\">{account.type}</p>\n                </>\n              )}\n            </div>\n          </div>\n\n          {/* Credit card secondary line: what you owe + limit, with a utilization bar */}\n          {isDebt && availableCredit !== null && (\n            <div className=\"mt-3 space-y-1.5\">\n              <div className=\"flex items-center justify-between text-xs text-muted-foreground\">\n                <span className=\"tabular-nums\">{fmtCurrency(owed)} owed</span>\n                {account.creditLimit != null ? (\n                  <span className=\"tabular-nums\">Limit {fmtCurrency(account.creditLimit)}</span>\n                ) : account.type === \"credit\" ? (\n                  <span>Limit not set</span>\n                ) : null}\n              </div>\n              {account.type === \"credit\" && account.creditLimit != null && account.creditLimit > 0 && (\n                <Progress\n                  value={Math.min(100, (owed / account.creditLimit) * 100)}\n                  className={cn(\"h-1.5\", (owed / account.creditLimit) * 100 < 60 ? \"[&>div]:bg-chart-green\" : \"[&>div]:bg-chart-orange\")}\n                />\n              )}\n            </div>\n          )}\n\n          <div className=\"mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-border/40 pt-3\">\n            <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n              {isPlaid(account) ? (\n                <>\n                  <RefreshCw size={12} strokeWidth={1.5} />\n                  <span>{syncLabel(account)}</span>\n                </>\n              ) : (\n                <span>Added manually</span>\n              )}\n            </div>\n            {/* Linked banks sync on their own, so there is no manual sync button. */}\n            {isPlaid(account) && plaidItem?.requires_reconnect ? (\n              <div onClick={event => event.stopPropagation()}>\n                <BankReconnectButton itemId={plaidItem.item_id} institution={plaidItem.institution_name || account.institution || \"Linked bank\"} onReconnected={syncPlaid} />\n              </div>\n            ) : isPlaid(account) ? null : (\n              <div className=\"flex items-center gap-1\">\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  className=\"h-6 px-2 text-[10px] uppercase font-semibold text-muted-foreground hover:text-primary tracking-wide\"\n                  onClick={(e) => { e.stopPropagation(); setEditorAccount(account); setEditorOpen(true); }}\n                >\n                  Edit\n                </Button>\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  className=\"h-6 px-2 text-[10px] uppercase font-semibold text-muted-foreground hover:text-chart-red tracking-wide\"\n                  onClick={(e) => { e.stopPropagation(); setConfirmDelete(account); }}\n                >\n                  Remove\n                </Button>\n              </div>\n            )}\n          </div>\n        </CardContent>\n      </Card>\n    );\n  };\n\n  /** What sits under an asset's big number: progress on a loan, gain/loss on a car or home. */\n  const assetDetail = (account: Account) => {\n    const fmtCurrency = (n: number) => formatMoney(Math.abs(n), account.currency || \"USD\");\n    if (account.type === \"loan\") {\n      const paid =\n        account.originalPrincipal && account.originalPrincipal > 0\n          ? Math.min(100, Math.max(0, ((account.originalPrincipal - Math.abs(account.balance)) / account.originalPrincipal) * 100))\n          : null;\n      const terms = [\n        account.monthlyPayment != null ? `${fmtCurrency(account.monthlyPayment)}/mo` : null,\n        account.interestRate != null ? `${account.interestRate}% APR` : null,\n      ].filter(Boolean).join(\" · \");\n      if (paid == null && !terms) return null;\n      return (\n        <div className=\"space-y-1.5\">\n          {paid != null && (\n            <>\n              <Progress value={paid} className=\"h-1.5 [&>div]:bg-chart-green\" />\n              <p className=\"text-xs text-muted-foreground tabular-nums\">{paid.toFixed(0)}% paid off{terms ? ` · ${terms}` : \"\"}</p>\n            </>\n          )}\n          {paid == null && <p className=\"text-xs text-muted-foreground tabular-nums\">{terms}</p>}\n        </div>\n      );\n    }\n    if (isIlliquidAsset(account.type)) {\n      if (account.purchasePrice != null) {\n        const gain = account.balance - account.purchasePrice;\n        return (\n          <p className=\"text-xs tabular-nums text-muted-foreground\">\n            <span className={gain >= 0 ? \"text-chart-green font-medium\" : \"text-chart-red font-medium\"}>\n              {gain >= 0 ? \"+\" : \"-\"}{fmtCurrency(gain)}\n            </span>{\" \"}since purchase ({fmtCurrency(account.purchasePrice)})\n          </p>\n        );\n      }\n      if (account.purchaseDate) return <p className=\"text-xs text-muted-foreground\">Purchased {account.purchaseDate}</p>;\n    }\n    return null;\n  };\n\n  const renderAssetCard = (account: Account) => {\n    const fmtCurrency = (n: number) => formatMoney(Math.abs(n), account.currency || \"USD\");\n    const Icon = typeIcons[account.type] || Landmark;\n    const isDebt = isDebtAccount(account.type);\n    const value = isDebt ? Math.abs(account.balance) : account.balance;\n    const caption = isDebt ? \"Owed\" : account.type === \"cash\" ? \"In cash\" : \"Current value\";\n    const detail = assetDetail(account);\n    const note = account.notes?.trim();\n    return (\n      <Card\n        key={account.id}\n        className=\"group cursor-pointer border-border/60 bg-gradient-to-br from-card to-card/50 hover:border-primary/40 hover:shadow-sm transition-all\"\n        onClick={() => navigate(`/accounts/${account.id}`)}\n      >\n        <CardContent className=\"p-5 flex h-full flex-col gap-4\">\n          <div className=\"flex items-start gap-3\">\n            <div className={cn(\"h-11 w-11 rounded-xl flex items-center justify-center shrink-0\", ACCOUNT_TINTS[account.type])}>\n              <Icon size={20} strokeWidth={1.5} />\n            </div>\n            <div className=\"min-w-0 flex-1\">\n              <div className=\"flex items-center gap-1.5\">\n                <p className=\"text-sm font-semibold text-foreground truncate\">{accountDisplayName(account)}</p>\n                <button\n                  className=\"opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity text-muted-foreground hover:text-primary shrink-0\"\n                  aria-label=\"Rename\"\n                  onClick={(e) => { e.stopPropagation(); openRename(account); }}\n                >\n                  <Pencil size={12} strokeWidth={1.5} />\n                </button>\n              </div>\n              <p className=\"text-xs text-muted-foreground truncate\">{accountSubtitle(account)}</p>\n            </div>\n          </div>\n\n          <div>\n            <p className={cn(\"text-2xl font-bold tabular-nums tracking-tight\", value < 0 ? \"text-chart-red\" : \"text-foreground\")}>\n              {value < 0 ? \"-\" : \"\"}{formatMoney(Math.abs(value), account.currency || \"USD\")}\n            </p>\n            <p className=\"text-xs text-muted-foreground mt-0.5\">{caption}</p>\n          </div>\n\n          {detail}\n\n          {note && (\n            <p className=\"flex gap-1.5 text-xs text-muted-foreground border-t border-border/40 pt-3\">\n              <NotebookPen size={12} strokeWidth={1.5} className=\"mt-0.5 shrink-0\" />\n              <span className=\"line-clamp-2 whitespace-pre-line\">{note}</span>\n            </p>\n          )}\n\n          <div className=\"mt-auto flex items-center justify-between pt-1 text-xs text-muted-foreground\">\n            <span>{isPlaid(account) ? syncLabel(account) : \"Added manually\"}</span>\n            {!isPlaid(account) && (\n              <span className=\"flex items-center gap-1 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity\">\n                <button\n                  className=\"px-1.5 py-0.5 rounded hover:text-primary hover:bg-primary/10\"\n                  onClick={(e) => { e.stopPropagation(); setEditorAccount(account); setEditorOpen(true); }}\n                >\n                  Edit\n                </button>\n                <button\n                  className=\"px-1.5 py-0.5 rounded hover:text-chart-red hover:bg-chart-red/10\"\n                  onClick={(e) => { e.stopPropagation(); setConfirmDelete(account); }}\n                >\n                  Remove\n                </button>\n              </span>\n            )}\n          </div>\n        </CardContent>\n      </Card>\n    );\n  };\n\n  // What you own, split for the composition bar (largest first), against what you owe.\n  const composition = [\n    { key: \"cash\", label: \"Cash\", value: sum(ofType(\"cash\")) },\n    { key: \"investment\", label: \"Investments\", value: sum(ofType(\"investment\")) },\n    { key: \"property\", label: \"Property & vehicles\", value: sum(ofType(\"property\", \"vehicle\")) },\n  ].filter((g) => g.value > 0).sort((a, b) => b.value - a.value);\n  const ownedPositive = composition.reduce((t, g) => t + g.value, 0);\n  const netAssets = ownedTotal - owedTotal;\n\n  return (\n    <Layout>\n      <div className=\"space-y-8\">\n        <div className=\"flex flex-col sm:flex-row sm:items-center justify-between gap-3\">\n          <div>\n            <h1 className=\"text-2xl font-bold text-foreground tracking-tight\">{pageTitle}</h1>\n            <p className=\"text-sm text-muted-foreground mt-0.5\">\n              {scoped.length} {isBank ? \"connected account\" : \"asset and loan\"}{scoped.length === 1 ? \"\" : \"s\"}\n            </p>\n          </div>\n          <div className=\"flex items-center gap-2\">\n            {isBank ? (\n              <ConnectBankButton />\n            ) : (\n              <Button variant=\"outline\" className=\"gap-2\" onClick={() => navigate(\"/investments\")}>\n                <TrendingUp size={16} strokeWidth={1.5} /> Brokerage portfolios\n              </Button>\n            )}\n            {!isBank && <Button\n              className=\"gap-2\"\n              onClick={() => { setEditorAccount(null); setEditorOpen(true); }}\n            >\n              <Plus size={16} strokeWidth={1.5} /> Add asset, cash or loan\n            </Button>}\n          </div>\n        </div>\n\n        {!isBank && (\n          <Card className=\"border-border/60 bg-gradient-to-br from-card to-card/40 overflow-hidden\">\n            <CardContent className=\"p-6 space-y-5\">\n              <div className=\"flex flex-wrap items-end justify-between gap-x-10 gap-y-4\">\n                <div>\n                  <p className=\"text-xs uppercase tracking-wider text-muted-foreground font-medium\">Net assets</p>\n                  <p className={cn(\"mt-1.5 text-4xl font-bold tabular-nums tracking-tight\", netAssets < 0 ? \"text-chart-red\" : \"text-foreground\")}>\n                    {netAssets < 0 ? \"-\" : \"\"}{fmtCurrency(netAssets)}\n                  </p>\n                </div>\n                <dl className=\"flex gap-8 text-sm\">\n                  <div>\n                    <dt className=\"text-xs text-muted-foreground\">You own</dt>\n                    <dd className=\"mt-0.5 font-semibold tabular-nums\">{fmtCurrency(ownedTotal)}</dd>\n                  </div>\n                  <div>\n                    <dt className=\"text-xs text-muted-foreground\">You owe</dt>\n                    <dd className={cn(\"mt-0.5 font-semibold tabular-nums\", owedTotal > 0 && \"text-chart-red\")}>{fmtCurrency(owedTotal)}</dd>\n                  </div>\n                </dl>\n              </div>\n              {composition.length > 0 && (\n                <div className=\"space-y-3\">\n                  <WeightBar\n                    label={`What you own: ${composition.map((g) => `${g.label} ${((g.value / ownedPositive) * 100).toFixed(0)}%`).join(\", \")}`}\n                    shares={composition.map((g) => ({ key: g.key, share: (g.value / ownedPositive) * 100 }))}\n                    className=\"h-2\"\n                  />\n                  <ul className=\"flex flex-wrap gap-x-6 gap-y-1.5 text-xs\">\n                    {composition.map((g, i) => (\n                      <li key={g.key} className=\"flex items-center gap-2\">\n                        <span aria-hidden className=\"h-2 w-2 rounded-full\" style={{ backgroundColor: shareColor(i) }} />\n                        <span className=\"text-muted-foreground\">{g.label}</span>\n                        <span className=\"font-semibold tabular-nums text-foreground\">{fmtCurrency(g.value)}</span>\n                        <span className=\"text-muted-foreground tabular-nums\">{((g.value / ownedPositive) * 100).toFixed(0)}%</span>\n                      </li>\n                    ))}\n                  </ul>\n                </div>\n              )}\n            </CardContent>\n          </Card>\n        )}\n\n        {sections.filter((sec) => sec.list.length > 0).map((sec) => (\n          <section key={sec.title} className=\"space-y-3\">\n            <div className=\"flex items-baseline justify-between px-1\">\n              <h2 className=\"text-xs uppercase tracking-wider font-semibold text-muted-foreground/60\">\n                {sec.title} <span className=\"ml-1 normal-case tracking-normal font-normal\">· {sec.list.length}</span>\n              </h2>\n              {!isBank && (\n                <span className={cn(\"text-xs font-semibold tabular-nums\", sec.title === \"Loans\" ? \"text-chart-red\" : \"text-foreground\")}>\n                  {sec.title === \"Loans\" ? `${fmtCurrency(owedTotal)} owed` : fmtCurrency(sum(sec.list))}\n                </span>\n              )}\n            </div>\n            <div className=\"grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4\">\n              {sec.list.map(isBank ? renderCard : renderAssetCard)}\n            </div>\n          </section>\n        ))}\n      </div>\n\n      {/* Rename / nickname dialog */}\n      <Dialog open={!!editAccount} onOpenChange={(o) => !o && setEditAccount(null)}>\n        <DialogContent className=\"sm:max-w-[400px]\">\n          <DialogHeader>\n            <DialogTitle>Rename account</DialogTitle>\n          </DialogHeader>\n          <div className=\"space-y-4 py-2\">\n            <div className=\"space-y-1.5\">\n              <Label>Display name</Label>\n              <Input\n                value={nickInput}\n                onChange={(e) => setNickInput(e.target.value)}\n                placeholder={editAccount?.name}\n                autoFocus\n                onKeyDown={(e) => { if (e.key === \"Enter\") void saveNickname(); }}\n              />\n              <p className=\"text-xs text-muted-foreground\">\n                A nickname just for you, it won't change anything at your bank. Clear it to use the original name ({editAccount?.name}).\n              </p>\n            </div>\n          </div>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setEditAccount(null)}>Cancel</Button>\n            <Button onClick={() => void saveNickname()} disabled={savingNick}>{savingNick ? \"Saving...\" : \"Save\"}</Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      <AccountDialog\n        open={editorOpen}\n        onOpenChange={(o) => { setEditorOpen(o); if (!o) setEditorAccount(null); }}\n        account={editorAccount}\n        types={scopeTypes}\n        onAdd={addAccount}\n        onUpdate={updateAccount}\n      />\n\n      <AlertDialog open={!!confirmDelete} onOpenChange={(o) => !o && setConfirmDelete(null)}>\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>Remove \"{confirmDelete && accountDisplayName(confirmDelete)}\"?</AlertDialogTitle>\n            <AlertDialogDescription>\n              The account is removed from your list. Transactions recorded against\n              it stay in your history, so your totals do not change.\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>Cancel</AlertDialogCancel>\n            <AlertDialogAction\n              className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n              onClick={async () => {\n                const target = confirmDelete;\n                setConfirmDelete(null);\n                if (!target) return;\n                try {\n                  await deleteAccount(target.id);\n                  toast.success(`${accountDisplayName(target)} removed`);\n                } catch {\n                  toast.error(\"Couldn't remove the account\");\n                }\n              }}\n            >\n              Remove\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </Layout>\n  );\n};\n"
    },
    {
      "name": "src/components/accounts/accountTints.ts",
      "type": "text/plain",
      "content": "/** The tile colour behind an account type's icon, shared by the Assets list and the account page. */\nexport const ACCOUNT_TINTS: Record<string, string> = {\n  checking: \"bg-chart-blue/10 text-chart-blue\",\n  savings: \"bg-chart-green/10 text-chart-green\",\n  credit: \"bg-chart-orange/10 text-chart-orange\",\n  investment: \"bg-chart-purple/10 text-chart-purple\",\n  loan: \"bg-chart-red/10 text-chart-red\",\n  vehicle: \"bg-chart-yellow/10 text-chart-yellow\",\n  property: \"bg-chart-yellow/10 text-chart-yellow\",\n  cash: \"bg-chart-green/10 text-chart-green\",\n};\n"
    },
    {
      "name": "src/components/analytics/CashflowChart.tsx",
      "type": "text/plain",
      "content": "import { GroupedBarChart, type BarSeries } from \"@/components/charts/GroupedBarChart\";\nimport { TooltipRow } from \"@/components/charts/ChartTooltip\";\nimport { useMoney } from \"@/hooks/useMoney\";\n\nimport type { CashflowPoint } from \"./cashflow-data\";\n\nconst SERIES: BarSeries<\"income\" | \"expenses\">[] = [\n  { key: \"income\", label: \"Income\", colorVar: \"--series-income\" },\n  { key: \"expenses\", label: \"Expenses\", colorVar: \"--series-expense\" },\n];\nconst getLabel = (d: CashflowPoint) => d.label;\nconst getValue = (d: CashflowPoint, key: \"income\" | \"expenses\") => d[key];\n\n/**\n * Income vs expenses per month.\n *\n * Two series, so this is the one genuinely categorical chart in the app. The\n * green/red pair is inherently the worst case for red-green colour blindness\n * (validated ΔE 6.7–7.2, inside the 6–8 floor band), which is legal only with\n * secondary encoding, hence the always-present legend, the gap between paired\n * bars, and the value labels in the tooltip.\n */\nexport function CashflowChart({ data }: { data: CashflowPoint[] }) {\n  const { money, compact } = useMoney();\n\n  if (data.length === 0) {\n    return <p className=\"text-sm text-muted-foreground text-center py-10\">No transactions to chart.</p>;\n  }\n\n  return (\n    <div className=\"space-y-3\">\n      {/* Legend is mandatory for two series, identity must never be colour alone. */}\n      <div className=\"flex items-center gap-4 text-xs\">\n        <span className=\"flex items-center gap-1.5\">\n          <span className=\"h-2.5 w-2.5 rounded-sm bg-series-income\" aria-hidden />\n          <span className=\"text-muted-foreground\">Income</span>\n        </span>\n        <span className=\"flex items-center gap-1.5\">\n          <span className=\"h-2.5 w-2.5 rounded-sm bg-series-expense\" aria-hidden />\n          <span className=\"text-muted-foreground\">Expenses</span>\n        </span>\n      </div>\n\n      {/* 2px gap between adjacent fills keeps the pair readable without colour. */}\n      <GroupedBarChart\n        data={data}\n        series={SERIES}\n        getLabel={getLabel}\n        getValue={getValue}\n        height={260}\n        formatAxisValue={compact}\n        renderTooltip={(d) => (\n          <>\n            <p className=\"font-semibold text-foreground\">{d.label}</p>\n            <TooltipRow color=\"hsl(var(--series-income))\" label=\"Income\" value={money(d.income)} />\n            <TooltipRow color=\"hsl(var(--series-expense))\" label=\"Expenses\" value={money(d.expenses)} />\n            <TooltipRow className=\"border-t border-border/40 pt-1\" label=\"Net\" value={money(d.income - d.expenses)} />\n          </>\n        )}\n      />\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/analytics/SpendByCategoryChart.tsx",
      "type": "text/plain",
      "content": "import { HorizontalBarChart } from \"@/components/charts/HorizontalBarChart\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { cn } from \"@/lib/utils\";\nimport type { Category } from \"@/lib/categories\";\n\nexport interface CategoryDatum {\n  name: Category;\n  value: number;\n  pct: number;\n}\n\nconst getName = (d: CategoryDatum) => d.name;\nconst getValue = (d: CategoryDatum) => d.value;\n\n/**\n * Spending by category as a sorted horizontal bar chart.\n *\n * Deliberately NOT the donut this app used to carry (which was never rendered\n * anywhere anyway). Two reasons:\n *   - Part-to-whole across ~20 long-named categories is a bar chart's job; a pie\n *     makes the reader compare angles and truncates its own labels.\n *   - The donut assigned colors by `index % 6` AFTER sorting by value, so a\n *     category's color changed whenever the ranking changed and repeated every\n *     7th slice. Bar length already encodes magnitude, so one hue is correct and\n *     colour never lies about identity.\n */\nexport function SpendByCategoryChart({\n  data,\n  onSelect,\n  selected,\n}: {\n  data: CategoryDatum[];\n  onSelect?: (category: Category) => void;\n  selected?: Category | null;\n}) {\n  const { money, compact } = useMoney();\n\n  if (data.length === 0) {\n    return <p className=\"text-sm text-muted-foreground text-center py-10\">No spending in this period.</p>;\n  }\n\n  return (\n    <HorizontalBarChart\n      data={data}\n      getLabel={getName}\n      getValue={getValue}\n      // One hue: bar length carries magnitude, so opacity is free to carry\n      // selection state instead of re-encoding the same number.\n      colorVar=\"--series-magnitude\"\n      // Direct labels, the relief channel the contrast check requires, and\n      // they remove the need to hover just to read a value.\n      formatValue={compact}\n      selected={selected}\n      onSelect={onSelect ? (d) => onSelect(d.name) : undefined}\n      renderTooltip={(d) => (\n        <>\n          <p className=\"font-semibold text-foreground\">{d.name}</p>\n          <p className=\"text-muted-foreground tabular-nums\">\n            {money(d.value)} · {d.pct.toFixed(1)}% of spend\n          </p>\n          {onSelect && <p className=\"text-primary\">Click to see transactions</p>}\n        </>\n      )}\n    />\n  );\n}\n\n/** The same numbers as an accessible table, the chart's non-visual equivalent. */\nexport function SpendByCategoryTable({ data }: { data: CategoryDatum[] }) {\n  const { money } = useMoney();\n  return (\n    <table className=\"w-full text-sm\">\n      <caption className=\"sr-only\">Spending by category</caption>\n      <thead>\n        <tr className=\"text-xs text-muted-foreground\">\n          <th scope=\"col\" className=\"text-left font-medium py-1.5\">Category</th>\n          <th scope=\"col\" className=\"text-right font-medium py-1.5\">Spent</th>\n          <th scope=\"col\" className=\"text-right font-medium py-1.5\">Share</th>\n        </tr>\n      </thead>\n      <tbody>\n        {data.map((d) => (\n          <tr key={d.name} className={cn(\"border-t border-border/30\")}>\n            <td className=\"py-1.5 text-foreground\">{d.name}</td>\n            <td className=\"py-1.5 text-right tabular-nums text-foreground\">{money(d.value)}</td>\n            <td className=\"py-1.5 text-right tabular-nums text-muted-foreground\">{d.pct.toFixed(1)}%</td>\n          </tr>\n        ))}\n      </tbody>\n    </table>\n  );\n}\n"
    },
    {
      "name": "src/components/analytics/cashflow-data.ts",
      "type": "text/plain",
      "content": "import { parseLocalDate } from \"@/lib/utils\";\nimport { monthKeyOf } from \"@/lib/format\";\nimport type { Transaction } from \"@/types/finance\";\nimport { countsAsSpend } from \"@/lib/transfers\";\n\nexport interface CashflowPoint {\n  key: string;\n  label: string;\n  income: number;\n  expenses: number;\n}\n\nexport function buildCashflow(transactions: Transaction[], months: number): CashflowPoint[] {\n  const byMonth = new Map<string, { income: number; expenses: number }>();\n  for (const t of transactions) {\n    if (!countsAsSpend(t)) continue;\n    const key = monthKeyOf(t.date);\n    if (!key || !Number.isFinite(t.amount)) continue;\n    if (!byMonth.has(key)) byMonth.set(key, { income: 0, expenses: 0 });\n    const bucket = byMonth.get(key)!;\n    if (t.type === \"income\") bucket.income += Math.abs(t.amount);\n    else bucket.expenses += Math.abs(t.amount);\n  }\n  return [...byMonth.entries()]\n    .sort(([a], [b]) => a.localeCompare(b))\n    .slice(-months)\n    .map(([key, v]) => {\n      const d = parseLocalDate(`${key}-01`);\n      return {\n        key,\n        // The year is included: a 6-month window crossing a year boundary used\n        // to render two identical \"Jan\" ticks.\n        label: d.toLocaleDateString(\"en-US\", { month: \"short\", year: \"2-digit\" }),\n        income: Math.round(v.income * 100) / 100,\n        expenses: Math.round(v.expenses * 100) / 100,\n      };\n    });\n}\n"
    },
    {
      "name": "src/components/auth/GoogleSignIn.tsx",
      "type": "text/plain",
      "content": "import { GoogleLogin, GoogleOAuthProvider, type GoogleLoginProps } from \"@react-oauth/google\";\n\nconst CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID || \"\";\nconst GSI_SCRIPT = \"https://accounts.google.com/gsi/client\";\n\n/** The Google button, with the provider that loads Google's ~100 kB sign-in\n * script. Mounting the provider here, not around the whole app, means that\n * script is only fetched and run where a visitor can actually sign in. */\nexport function GoogleSignIn(props: GoogleLoginProps) {\n  return (\n    <GoogleOAuthProvider clientId={CLIENT_ID}>\n      <GoogleLogin {...props} />\n    </GoogleOAuthProvider>\n  );\n}\n\n/** Download Google's script ahead of the sign-in dialog without running it,\n * so the button appears at once when the dialog opens. */\nexport function prefetchGoogleSignIn() {\n  if (!CLIENT_ID || document.querySelector(`link[href=\"${GSI_SCRIPT}\"]`)) return;\n  const link = document.createElement(\"link\");\n  link.rel = \"preload\";\n  link.as = \"script\";\n  link.href = GSI_SCRIPT;\n  document.head.appendChild(link);\n}\n"
    },
    {
      "name": "src/components/brand/Logo.tsx",
      "type": "text/plain",
      "content": "import { cn } from \"@/lib/utils\";\n\n/**\n * Architype Stedelijk: the five khmy$ glyph outlines from The Foundry's\n * specimen, set at their original advance widths with no added tracking.\n * Source: https://www.thefoundrytypes.com/fonts/architype-stedelijk/\n * Only the fixed logo artwork is included; no font software is bundled.\n */\nconst WORDMARK_PATH = [\n  \"M17 667 Q10 667 5 662 Q0 657 0 650 L0 17 Q0 10 5 5 Q10 0 17 0 L78 0 Q85 0 90 5 Q95 10 95 17 L95 364 Q95 371 100 376 Q105 381 112 381 L269 381 Q276 381 281 376 Q286 371 286 364 L286 208 Q286 201 291 196 Q296 191 303 191 L364 191 Q371 191 376 196 Q381 201 381 208 L381 364 Q381 371 376 376 Q371 381 364 381 L303 381 Q296 381 291 386 Q286 391 286 398 L286 460 Q286 467 291 472 Q296 477 303 477 L364 477 Q371 477 376 482 Q381 487 381 494 L381 650 Q381 657 376 662 Q371 667 364 667 L303 667 Q296 667 291 662 Q286 657 286 650 L286 494 Q286 487 281 482 Q276 477 269 477 L112 477 Q105 477 100 482 Q95 487 95 494 L95 650 Q95 657 90 662 Q85 667 78 667 L17 667 Z\",\n  \"M422 667 Q415 667 410 662 Q405 657 405 650 L405 17 Q405 10 410 5 Q415 0 422 0 L483 0 Q490 0 495 5 Q500 10 500 17 L500 174 Q500 181 505 186 Q510 191 517 191 L674 191 Q681 191 686 196 Q691 201 691 208 L691 269 Q691 276 696 281 Q701 286 708 286 L769 286 Q776 286 781 291 Q786 296 786 303 L786 650 Q786 657 781 662 Q776 667 769 667 L708 667 Q701 667 696 662 Q691 657 691 650 L691 303 Q691 296 686 291 Q681 286 674 286 L517 286 Q510 286 505 291 Q500 296 500 303 L500 650 Q500 657 495 662 Q490 667 483 667 L422 667 Z\",\n  \"M827 667 Q820 667 815 662 Q810 657 810 650 L810 208 Q810 201 815 196 Q820 191 827 191 L1079 191 Q1086 191 1091 196 Q1096 201 1096 208 L1096 269 Q1096 276 1101 281 Q1106 286 1113 286 L1174 286 Q1181 286 1186 281 Q1191 276 1191 269 L1191 208 Q1191 201 1196 196 Q1201 191 1208 191 L1365 191 Q1372 191 1377 196 Q1382 201 1382 208 L1382 269 Q1382 276 1387 281 Q1392 286 1399 286 L1460 286 Q1467 286 1472 291 Q1477 296 1477 303 L1477 650 Q1477 657 1472 662 Q1467 667 1460 667 L1399 667 Q1392 667 1387 662 Q1382 657 1382 650 L1382 303 Q1382 296 1377 291 Q1372 286 1365 286 L1208 286 Q1201 286 1196 291 Q1191 296 1191 303 L1191 650 Q1191 657 1186 662 Q1181 667 1174 667 L1113 667 Q1106 667 1101 662 Q1096 657 1096 650 L1096 303 Q1096 296 1091 291 Q1086 286 1079 286 L922 286 Q915 286 910 291 Q905 296 905 303 L905 650 Q905 657 900 662 Q895 667 888 667 L827 667 Z\",\n  \"M1613 667 Q1606 667 1601 662 Q1596 657 1596 650 L1596 589 Q1596 582 1591 577 Q1586 572 1579 572 L1518 572 Q1511 572 1506 567 Q1501 562 1501 555 L1501 208 Q1501 201 1506 196 Q1511 191 1518 191 L1579 191 Q1586 191 1591 196 Q1596 201 1596 208 L1596 555 Q1596 562 1601 567 Q1606 572 1613 572 L1770 572 Q1777 572 1782 567 Q1787 562 1787 555 L1787 208 Q1787 201 1792 196 Q1797 191 1804 191 L1865 191 Q1872 191 1877 196 Q1882 201 1882 208 L1882 745 Q1882 752 1877 757 Q1872 762 1865 762 L1804 762 Q1797 762 1792 767 Q1787 772 1787 779 L1787 840 Q1787 847 1782 852 Q1777 857 1770 857 L1518 857 Q1511 857 1506 852 Q1501 847 1501 840 L1501 779 Q1501 772 1506 767 Q1511 762 1518 762 L1770 762 Q1777 762 1782 757 Q1787 752 1787 745 L1787 684 Q1787 677 1782 672 Q1777 667 1770 667 L1613 667 Z\",\n  \"M1923 667 Q1916 667 1911 662 Q1906 657 1906 650 L1906 589 Q1906 582 1911 577 Q1916 572 1923 572 L2175 572 Q2182 572 2187 567 Q2192 562 2192 555 L2192 494 Q2192 487 2187 482 Q2182 477 2175 477 L1923 477 Q1916 477 1911 472 Q1906 467 1906 460 L1906 303 Q1906 296 1911 291 Q1916 286 1923 286 L1984 286 Q1991 286 1996 281 Q2001 276 2001 269 L2001 208 Q2001 201 2006 196 Q2011 191 2018 191 L2079 191 Q2086 191 2091 186 Q2096 181 2096 174 L2096 113 Q2096 106 2101 101 Q2106 96 2113 96 L2175 96 Q2182 96 2187 101 Q2192 106 2192 113 L2192 174 Q2192 181 2197 186 Q2202 191 2209 191 L2270 191 Q2277 191 2282 196 Q2287 201 2287 208 L2287 269 Q2287 276 2282 281 Q2277 286 2270 286 L2018 286 Q2011 286 2006 291 Q2001 296 2001 303 L2001 364 Q2001 371 2006 376 Q2011 381 2018 381 L2270 381 Q2277 381 2282 386 Q2287 391 2287 398 L2287 555 Q2287 562 2282 567 Q2277 572 2270 572 L2209 572 Q2202 572 2197 577 Q2192 582 2192 589 L2192 745 Q2192 752 2187 757 Q2182 762 2175 762 L2113 762 Q2106 762 2101 757 Q2096 752 2096 745 L2096 684 Q2096 677 2091 672 Q2086 667 2079 667 L1923 667 Z\",\n].join(\" \");\n\nconst MONOGRAM_PATH = \"M17 667 Q10 667 5 662 Q0 657 0 650 L0 17 Q0 10 5 5 Q10 0 17 0 L78 0 Q85 0 90 5 Q95 10 95 17 L95 364 Q95 371 100 376 Q105 381 112 381 L269 381 Q276 381 281 376 Q286 371 286 364 L286 208 Q286 201 291 196 Q296 191 303 191 L364 191 Q371 191 376 196 Q381 201 381 208 L381 364 Q381 371 376 376 Q371 381 364 381 L303 381 Q296 381 291 386 Q286 391 286 398 L286 460 Q286 467 291 472 Q296 477 303 477 L364 477 Q371 477 376 482 Q381 487 381 494 L381 650 Q381 657 376 662 Q371 667 364 667 L303 667 Q296 667 291 662 Q286 657 286 650 L286 494 Q286 487 281 482 Q276 477 269 477 L112 477 Q105 477 100 482 Q95 487 95 494 L95 650 Q95 657 90 662 Q85 667 78 667 L17 667 Z\";\n\nexport type LogoTone = \"landing\" | \"app\" | \"mono\" | \"inverse\" | \"onPrimary\" | \"teal\";\n\nconst TONES: Record<LogoTone, string> = {\n  landing: \"brand-wordmark\",\n  app: \"brand-wordmark\",\n  mono: \"text-foreground\",\n  inverse: \"text-background\",\n  onPrimary: \"text-primary-foreground\",\n  teal: \"text-[hsl(174_60%_40%)]\",\n};\n\n/** Architype Stedelijk khmy$. Size by height to preserve the letterforms. */\nexport function Logo({ className, tone = \"landing\" }: { className?: string; tone?: LogoTone }) {\n  return (\n    <svg viewBox=\"0 0 2287 857\" role=\"img\" aria-label=\"KHMYS\" focusable=\"false\" className={cn(\"h-7 w-auto shrink-0\", TONES[tone], className)} fill=\"currentColor\">\n      <path d={WORDMARK_PATH} />\n    </svg>\n  );\n}\n\n/** The same Architype Stedelijk k for square browser icons and avatars. */\nexport function LogoMark({ className, tone = \"landing\" }: { className?: string; tone?: LogoTone }) {\n  return (\n    <svg viewBox=\"-238 -95 857 857\" role=\"img\" aria-label=\"KHMYS\" focusable=\"false\" className={cn(\"h-7 w-7 shrink-0\", TONES[tone], className)} fill=\"currentColor\">\n      <path d={MONOGRAM_PATH} />\n    </svg>\n  );\n}\n\n/** Standalone outlined wordmark for brand-kit downloads. */\nexport function logoSvg(color: string): string {\n  if (!/^#[0-9a-f]{6}$/i.test(color)) throw new Error(\"Logo export requires a six-digit hex color\");\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2287 857\" role=\"img\" aria-label=\"KHMYS\"><path fill=\"${color}\" d=\"${WORDMARK_PATH}\"/></svg>\\n`;\n}\n\n/** Browser icon using the same Stedelijk k, white against a dark surface. */\nexport function faviconSvg(): string {\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"-238 -95 857 857\"><rect x=\"-238\" y=\"-95\" width=\"857\" height=\"857\" rx=\"170\" fill=\"#12312C\"/><path fill=\"#FFFFFF\" d=\"${MONOGRAM_PATH}\"/></svg>\\n`;\n}\n"
    },
    {
      "name": "src/components/brand/PublicFooter.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useState, type ReactNode } from \"react\";\nimport { ArrowUpRight } from \"lucide-react\";\nimport { Logo } from \"@/components/brand/Logo\";\nimport { APP_ORIGIN, DEMO_ORIGIN } from \"@/lib/deployment\";\n\ntype Palette = \"landing-dark\" | \"landing-light\";\n\n/** \"landing-dark\" when the page is in dark mode, tracking the root class as it changes. */\nexport function useRootPalette(): Palette {\n  // The static branding render runs without a DOM; default to light there.\n  const read = (): Palette =>\n    typeof document !== \"undefined\" && document.documentElement.classList.contains(\"dark\") ? \"landing-dark\" : \"landing-light\";\n  const [palette, setPalette] = useState<Palette>(read);\n  useEffect(() => {\n    const observer = new MutationObserver(() => setPalette(read()));\n    observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n    setPalette(read());\n    return () => observer.disconnect();\n  }, []);\n  return palette;\n}\n\nconst link = \"inline-flex min-h-[44px] items-center hover:text-land-fg\";\n\n/**\n * The footer shared by every public page: wordmark, legal and brand links,\n * credits and the large teal wordmark on a black panel (black in both themes).\n * It reads the landing palette tokens, so it needs a `landing-*` scope: pass\n * `palette` to wrap it in one, or omit it inside a page that already has one.\n * `onSignIn` opens a sign-in dialog; without it, \"Sign in\" links to the site.\n */\nexport function PublicFooter({ palette, onSignIn }: { palette?: Palette; onSignIn?: () => void }) {\n  const body: ReactNode = (\n    <footer className=\"mx-auto max-w-[1280px] px-5 pb-8 sm:px-8 lg:px-10\">\n      <div className=\"flex flex-col justify-between gap-5 border-t border-land-line pt-7 sm:flex-row sm:items-center\">\n        <div className=\"flex flex-wrap items-center gap-2\">\n          <Logo className=\"h-6\" />\n          <span className=\"ml-3 text-[11px] text-land-muted\">Know How Much You Spend.</span>\n        </div>\n        <div className=\"flex flex-wrap items-center gap-x-6 gap-y-2 text-[11px] text-land-muted\">\n          <a href={`${APP_ORIGIN}/blog`} className={link}>Guides</a>\n          <a href={`${APP_ORIGIN}/about`} className={link}>About</a>\n          <a href={DEMO_ORIGIN} className={link}>Explore the demo</a>\n          <a href={`${APP_ORIGIN}/branding`} className={link}>Brand kit</a>\n          <a href={`${APP_ORIGIN}/privacy`} className={link}>Privacy</a>\n          <a href={`${APP_ORIGIN}/terms`} className={link}>Terms</a>\n          <a href={`${APP_ORIGIN}/security`} className={link}>Security</a>\n          <a href=\"mailto:support@khmys.com\" className={link}>Support</a>\n          {onSignIn ? (\n            <button onClick={onSignIn} className={link}>Sign in</button>\n          ) : (\n            <a href={APP_ORIGIN} className={link}>Sign in</a>\n          )}\n        </div>\n      </div>\n      <div className=\"mt-7 flex flex-col justify-between gap-3 border-t border-land-line/60 pt-5 text-[11px] text-land-muted sm:flex-row sm:items-center\">\n        <p>© 2026 KHMYS.</p>\n        <p>\n          Developed by{\" \"}\n          <a\n            href=\"https://snehithkumar.com\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"inline-flex items-center gap-1 font-medium text-land-fg underline decoration-land-line underline-offset-4 transition hover:text-land-accent\"\n          >\n            Snehith Kumar <ArrowUpRight size={12} aria-hidden />\n          </a>\n        </p>\n      </div>\n      <div className=\"mt-10 overflow-hidden rounded-3xl bg-black px-6 py-10 sm:mt-12 sm:px-12 sm:py-14\" aria-label=\"KHMYS footer wordmark\">\n        <Logo tone=\"teal\" className=\"h-auto w-full\" />\n      </div>\n    </footer>\n  );\n  return palette ? <div className={`${palette} pt-16 text-land-fg`}>{body}</div> : body;\n}\n"
    },
    {
      "name": "src/components/charts/BubbleChart.tsx",
      "type": "text/plain",
      "content": "import { useMemo, useState } from \"react\";\nimport { Pack, hierarchy } from \"@visx/hierarchy\";\nimport { Group } from \"@visx/group\";\nimport type { DonutSlice } from \"./DonutChart\";\n\ninterface Props {\n  data: DonutSlice[];\n  size: number;\n  formatValue: (n: number) => string;\n}\n\n/** Synthetic root d3-hierarchy packs against; never rendered itself. */\ninterface Root {\n  children: DonutSlice[];\n}\n\n/**\n * The same DonutSlice[] a DonutChart takes, laid out as proportional\n * circles instead of a ring. A caller toggling between the two swaps one\n * component for the other with no data reshaping.\n */\nexport function BubbleChart({ data, size, formatValue }: Props) {\n  const [active, setActive] = useState<string | null>(null);\n  const total = data.reduce((s, d) => s + d.value, 0);\n\n  const root = useMemo(() => {\n    const shown = data.filter((d) => d.value > 0);\n    return hierarchy<Root>({ children: shown })\n      .sum((d) => (\"value\" in d ? (d as unknown as DonutSlice).value : 0))\n      .sort((a, b) => (b.value ?? 0) - (a.value ?? 0));\n  }, [data]);\n\n  if (data.every((d) => d.value <= 0)) return null;\n\n  return (\n    <div className=\"relative\" style={{ width: size, height: size }}>\n      <svg width={size} height={size} role=\"group\" aria-label={`${data.length} categories`}>\n        <Pack root={root} size={[size, size]} padding={3}>\n          {(packed) => (\n            <Group>\n              {packed\n                .descendants()\n                .filter((d) => d.depth > 0)\n                .map((c) => {\n                  const d = c.data as unknown as DonutSlice;\n                  const isActive = active === d.name;\n                  const pct = total > 0 ? (d.value / total) * 100 : 0;\n                  const r = c.r;\n                  const showLabel = r > 22;\n                  return (\n                    <g\n                      key={d.name}\n                      transform={`translate(${c.x}, ${c.y})`}\n                      tabIndex={0}\n                      role=\"img\"\n                      aria-label={`${d.name}: ${formatValue(d.value)}, ${pct.toFixed(1)}%`}\n                      className=\"outline-none cursor-pointer\"\n                      onPointerEnter={() => setActive(d.name)}\n                      onPointerLeave={() => setActive(null)}\n                      onFocus={() => setActive(d.name)}\n                      onBlur={() => setActive(null)}\n                    >\n                      <circle\n                        r={r}\n                        fill={d.color}\n                        opacity={active && !isActive ? 0.5 : 0.92}\n                        stroke=\"hsl(var(--card))\"\n                        strokeWidth={2}\n                        style={{ transition: \"opacity 150ms ease\" }}\n                      />\n                      {showLabel && (\n                        <text textAnchor=\"middle\" dy=\"-0.15em\" fontSize={Math.min(15, r / 2.6)} fontWeight={700} fill=\"hsl(var(--card))\">\n                          {pct.toFixed(0)}%\n                        </text>\n                      )}\n                      {showLabel && (\n                        <text textAnchor=\"middle\" dy=\"1.2em\" fontSize={Math.min(11, r / 3.6)} fill=\"hsl(var(--card))\" opacity={0.9}>\n                          {d.name}\n                        </text>\n                      )}\n                    </g>\n                  );\n                })}\n            </Group>\n          )}\n        </Pack>\n      </svg>\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/charts/CalendarHeatmap.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useId, useMemo, useRef, useState, type KeyboardEvent } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { format } from \"date-fns\";\nimport { cn, parseLocalDate } from \"@/lib/utils\";\nimport {\n  activityLevel, activityThresholds, calendarWeeks, inCalendarSelection, orderedSelection,\n  type ActivityDay, type CalendarSelection, type FlowDirection,\n} from \"@/lib/calendarActivity\";\n\ninterface Props {\n  year: number;\n  days: ReadonlyMap<string, ActivityDay>;\n  direction: FlowDirection;\n  selection: CalendarSelection | null;\n  onSelect: (selection: CalendarSelection | null) => void;\n  formatValue: (amount: number) => string;\n  today: string;\n  /** A transaction ledger may contain future-dated entries; Home only plots posted activity. */\n  allowFuture?: boolean;\n}\n\nconst levelClasses = [\n  \"bg-muted/80 dark:bg-white/[0.035]\",\n  \"bg-teal-200 dark:bg-teal-900/60\",\n  \"bg-teal-400/80 dark:bg-teal-700\",\n  \"bg-teal-500 dark:bg-teal-500/80\",\n  \"bg-teal-700 dark:bg-teal-300\",\n];\n\nexport function CalendarHeatmap({ year, days, direction, selection, onSelect, formatValue, today, allowFuture = false }: Props) {\n  const weeks = useMemo(() => calendarWeeks(year), [year]);\n  const dates = useMemo(() => weeks.flat().filter((d): d is string => !!d && (allowFuture || d <= today)), [weeks, today, allowFuture]);\n  const thresholds = useMemo(() => activityThresholds(days, direction), [days, direction]);\n  const grid = useRef<HTMLDivElement>(null);\n  const drag = useRef<{ anchor: string; end: string; id: number } | null>(null);\n  const skipClick = useRef(false);\n  const resetClickTimer = useRef<ReturnType<typeof setTimeout>>();\n  const [preview, setPreview] = useState<CalendarSelection | null>(null);\n  const [focused, setFocused] = useState(today.startsWith(String(year)) ? today : `${year}-01-01`);\n  const [hover, setHover] = useState<{ date: string; x: number; y: number } | null>(null);\n  const hintId = useId();\n  const tooltipId = useId();\n  const active = preview ?? selection;\n  const focusDate = dates.includes(focused) ? focused : dates[0];\n  const lastOnSelect = useRef(onSelect);\n  lastOnSelect.current = onSelect;\n\n  useEffect(() => {\n    const finish = (event: PointerEvent) => {\n      const current = drag.current;\n      if (!current || current.id !== event.pointerId) return;\n      if (current.anchor !== current.end) {\n        lastOnSelect.current(orderedSelection(current.anchor, current.end));\n        skipClick.current = true;\n        // The browser's synthetic click follows pointerup in the same task.\n        // A release outside the chart must not swallow the next real click.\n        resetClickTimer.current = setTimeout(() => { skipClick.current = false; }, 0);\n      }\n      drag.current = null;\n      setPreview(null);\n    };\n    const cancel = () => { drag.current = null; setPreview(null); };\n    const escape = (e: globalThis.KeyboardEvent) => { if (e.key === \"Escape\") cancel(); };\n    const dismiss = () => setHover(null);\n    window.addEventListener(\"pointerup\", finish);\n    window.addEventListener(\"pointercancel\", cancel);\n    window.addEventListener(\"blur\", cancel);\n    window.addEventListener(\"keydown\", escape);\n    window.addEventListener(\"scroll\", dismiss, true);\n    window.addEventListener(\"resize\", dismiss);\n    return () => {\n      window.removeEventListener(\"pointerup\", finish);\n      window.removeEventListener(\"pointercancel\", cancel);\n      window.removeEventListener(\"blur\", cancel);\n      window.removeEventListener(\"keydown\", escape);\n      window.removeEventListener(\"scroll\", dismiss, true);\n      window.removeEventListener(\"resize\", dismiss);\n      clearTimeout(resetClickTimer.current);\n    };\n  }, []);\n\n  const showTooltip = (date: string, target: HTMLElement) => {\n    const rect = target.getBoundingClientRect();\n    setHover({ date, x: rect.left + rect.width / 2, y: rect.top });\n  };\n\n  const toggleDay = (date: string) => {\n    onSelect(selection?.from === date && selection.to === date ? null : { from: date, to: date });\n  };\n\n  const keyboard = (event: KeyboardEvent<HTMLButtonElement>, date: string) => {\n    if (event.key === \"Escape\") {\n      event.preventDefault(); event.stopPropagation();\n      drag.current = null; setPreview(null); setHover(null); onSelect(null);\n      return;\n    }\n    const index = dates.indexOf(date);\n    const weekday = new Date(`${date}T00:00:00Z`).getUTCDay();\n    const next = event.key === \"ArrowLeft\" ? index - 7 : event.key === \"ArrowRight\" ? index + 7\n      : event.key === \"ArrowUp\" ? index - 1 : event.key === \"ArrowDown\" ? index + 1\n      : event.key === \"Home\" ? (event.ctrlKey || event.metaKey ? 0 : index - weekday)\n      : event.key === \"End\" ? (event.ctrlKey || event.metaKey ? dates.length - 1 : index + 6 - weekday) : null;\n    if (next === null) return;\n    event.preventDefault(); event.stopPropagation();\n    const target = dates[Math.max(0, Math.min(dates.length - 1, next))];\n    setFocused(target);\n    grid.current?.querySelector<HTMLButtonElement>(`[data-date=\"${target}\"]`)?.focus();\n  };\n\n  const hovered = hover ? days.get(hover.date) : undefined;\n  const resetHint = selection?.from === hover?.date && selection?.to === hover?.date;\n  const tooltip = hover && typeof document !== \"undefined\" ? createPortal(\n    <div\n      id={tooltipId} role=\"tooltip\"\n      className=\"pointer-events-none fixed z-50 w-[250px] max-w-[calc(100vw-24px)] rounded-xl border border-border bg-popover px-4 py-3 text-popover-foreground shadow-xl\"\n      style={{ left: Math.max(12, Math.min(hover.x - 125, window.innerWidth - 262)), top: hover.y > 140 ? hover.y - 12 : hover.y + 28, transform: hover.y > 140 ? \"translateY(-100%)\" : undefined }}\n    >\n      <p className=\"text-sm font-medium\">{format(parseLocalDate(hover.date), \"EEEE, MMMM d, yyyy\")}</p>\n      <p className=\"mt-1 text-sm text-muted-foreground tabular-nums\">{formatValue(hovered?.[direction] ?? 0)} {direction}</p>\n      <p className=\"mt-2 text-[11px] text-muted-foreground\">{resetHint ? \"Press to reset\" : \"Press to select day · Drag for range\"}</p>\n    </div>, document.body,\n  ) : null;\n\n  return (\n    <div>\n      <p id={hintId} className=\"sr-only\">Use arrow keys to explore days, Enter or Space to select a day, and Escape to clear. Drag across days or use the date picker to select a range.</p>\n      <div className=\"max-w-full overflow-x-auto overscroll-x-contain pb-2\" onPointerLeave={() => setHover(null)}>\n        <div ref={grid} role=\"group\" aria-label={`${year} daily ${direction} calendar heatmap`} aria-describedby={hintId} className=\"select-none\" style={{ minWidth: Math.max(660, weeks.length * 15 + 30) }}>\n          <div className=\"mb-2 grid pl-8 text-[10px] text-muted-foreground\" aria-hidden style={{ gridTemplateColumns: `repeat(${weeks.length}, minmax(0, 1fr))` }}>\n            {weeks.map((week, i) => {\n              const first = week.find(d => d?.endsWith(\"-01\"));\n              return <span key={i} className=\"overflow-visible whitespace-nowrap\">{first ? format(parseLocalDate(first), \"MMM\") : \"\"}</span>;\n            })}\n          </div>\n          <div className=\"flex gap-2\">\n            <div aria-hidden className=\"grid w-6 shrink-0 grid-rows-[repeat(7,minmax(0,1fr))] gap-[3px] text-[9px] text-muted-foreground\">\n              {[\"\", \"Mon\", \"\", \"Wed\", \"\", \"Fri\", \"\"].map((label, i) => <span key={i} className=\"flex items-center\">{label}</span>)}\n            </div>\n            <div className=\"grid min-w-0 flex-1 gap-[3px]\" style={{ gridTemplateColumns: `repeat(${weeks.length}, minmax(0, 1fr))` }}>\n              {weeks.map((week, i) => (\n                <div key={i} className=\"grid grid-rows-[repeat(7,minmax(0,1fr))] gap-[3px]\">\n                  {week.map((date, j) => {\n                    if (!date) return <span key={j} className=\"aspect-square max-h-[18px]\" aria-hidden />;\n                    const future = date > today;\n                    const disabled = future && !allowFuture;\n                    const amount = days.get(date)?.[direction] ?? 0;\n                    const level = activityLevel(amount, thresholds);\n                    const selected = !!active && inCalendarSelection(date, active);\n                    return (\n                      <button\n                        key={date} type=\"button\" data-date={date} data-level={level}\n                        aria-label={`${format(parseLocalDate(date), \"MMMM d, yyyy\")}: ${formatValue(amount)} ${direction}${future ? \", future date\" : \"\"}`}\n                        aria-pressed={selected} aria-describedby={hover?.date === date ? tooltipId : undefined}\n                        disabled={disabled} tabIndex={!disabled && focusDate === date ? 0 : -1}\n                        className={cn(\"aspect-square max-h-[18px] w-full rounded-full transition-[opacity,box-shadow] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500\",\n                          levelClasses[level], disabled && \"opacity-30\", active && !selected && \"opacity-20\",\n                          selected && \"ring-1 ring-teal-600 ring-offset-1 ring-offset-card dark:ring-teal-300\",\n                          !disabled && \"hover:ring-2 hover:ring-teal-500/60 hover:ring-offset-1 hover:ring-offset-card\")}\n                        onFocus={event => { setFocused(date); showTooltip(date, event.currentTarget); }}\n                        onBlur={() => setHover(null)}\n                        onPointerEnter={event => {\n                          if (disabled) return;\n                          showTooltip(date, event.currentTarget);\n                          if (drag.current && drag.current.id === event.pointerId) {\n                            drag.current.end = date;\n                            setPreview(orderedSelection(drag.current.anchor, date));\n                          }\n                        }}\n                        onPointerDown={event => {\n                          if (event.button !== 0 || event.pointerType === \"touch\") return;\n                          drag.current = { anchor: date, end: date, id: event.pointerId };\n                          // Avoid implicit touch/pen capture swallowing enter events.\n                          if (event.currentTarget.hasPointerCapture?.(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);\n                        }}\n                        onClick={() => { if (!skipClick.current) toggleDay(date); }}\n                        onKeyDown={event => keyboard(event, date)}\n                      />\n                    );\n                  })}\n                </div>\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n      {tooltip}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/charts/ChartTooltip.tsx",
      "type": "text/plain",
      "content": "import type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst TOOLTIP_WIDTH_GUESS = 200;\nconst OFFSET = 12;\n\n/**\n * The one tooltip panel every chart uses, positioned inside the chart's\n * `relative` wrapper. It flips to the left of the pointer near the right edge\n * so it never spills out of the card.\n */\nexport function ChartTooltip({\n  x,\n  y,\n  containerWidth,\n  children,\n}: {\n  x: number;\n  y: number;\n  containerWidth: number;\n  children: ReactNode;\n}) {\n  const flip = x + OFFSET + TOOLTIP_WIDTH_GUESS > containerWidth;\n  return (\n    <div\n      role=\"presentation\"\n      className=\"pointer-events-none absolute z-20 whitespace-nowrap\"\n      style={{\n        left: flip ? undefined : x + OFFSET,\n        right: flip ? containerWidth - x + OFFSET : undefined,\n        top: Math.max(0, y - 8),\n      }}\n    >\n      <div className=\"rounded-lg border border-border bg-popover/95 backdrop-blur-sm px-3 py-2 shadow-xl text-xs space-y-1\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\n/** A tooltip row: series key (a short line or square of its colour), name, value. */\nexport function TooltipRow({\n  color,\n  label,\n  value,\n  mark = \"rect\",\n  className,\n}: {\n  color?: string;\n  label: ReactNode;\n  value: ReactNode;\n  mark?: \"rect\" | \"line\";\n  className?: string;\n}) {\n  return (\n    <p className={cn(\"flex items-center justify-between gap-4\", className)}>\n      <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n        {color && (\n          <span\n            aria-hidden\n            className={mark === \"line\" ? \"h-0.5 w-3 rounded-full\" : \"h-2 w-2 rounded-sm\"}\n            style={{ backgroundColor: color }}\n          />\n        )}\n        {label}\n      </span>\n      <span className=\"tabular-nums font-medium text-foreground\">{value}</span>\n    </p>\n  );\n}\n"
    },
    {
      "name": "src/components/charts/ChartTypeToggle.tsx",
      "type": "text/plain",
      "content": "import type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Icon-only sibling of SegmentedControl: switches a chart's visual form\n * (line/bar, donut/bubbles) rather than its data range, so it reads as a\n * distinct, smaller control next to the period switcher rather than a second\n * one of the same kind.\n */\nexport function ChartTypeToggle<T extends string>({\n  options,\n  value,\n  onChange,\n  label,\n  className,\n}: {\n  options: readonly { value: T; icon: LucideIcon; label: string }[];\n  value: T;\n  onChange: (value: NoInfer<T>) => void;\n  label: string;\n  className?: string;\n}) {\n  return (\n    <div\n      role=\"radiogroup\"\n      aria-label={label}\n      className={cn(\"flex items-center gap-0.5 rounded-md border border-border/60 p-0.5 shrink-0\", className)}\n    >\n      {options.map((o) => (\n        <button\n          key={o.value}\n          type=\"button\"\n          role=\"radio\"\n          aria-checked={value === o.value}\n          aria-label={o.label}\n          tabIndex={value === o.value ? 0 : -1}\n          title={o.label}\n          onClick={() => onChange(o.value)}\n          onKeyDown={event => {\n            const index = options.findIndex(option => option.value === o.value);\n            const next = event.key === \"ArrowRight\" || event.key === \"ArrowDown\" ? (index + 1) % options.length\n              : event.key === \"ArrowLeft\" || event.key === \"ArrowUp\" ? (index - 1 + options.length) % options.length\n              : event.key === \"Home\" ? 0 : event.key === \"End\" ? options.length - 1 : null;\n            if (next === null) return;\n            event.preventDefault();\n            onChange(options[next].value);\n            event.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>('[role=\"radio\"]')[next]?.focus();\n          }}\n          className={cn(\n            \"flex items-center justify-center h-6 w-6 rounded transition-colors\",\n            value === o.value\n              ? \"bg-muted text-foreground\"\n              : \"text-muted-foreground hover:text-foreground hover:bg-muted/50\"\n          )}\n        >\n          <o.icon size={13} strokeWidth={1.75} />\n        </button>\n      ))}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/charts/DonutChart.tsx",
      "type": "text/plain",
      "content": "import { useState, type PointerEvent, type ReactNode } from \"react\";\nimport { Pie } from \"@visx/shape\";\nimport { Group } from \"@visx/group\";\nimport { localPoint } from \"@visx/event\";\nimport { ChartTooltip, TooltipRow } from \"./ChartTooltip\";\n\nexport interface DonutBreakdownPart {\n  key: string;\n  label: string;\n  /** Positive magnitude; parts should sum to (or near) the slice's own value. */\n  value: number;\n  /** Name of an HSL colour token, e.g. \"--chart-green\". */\n  colorVar: string;\n}\n\nexport interface DonutSlice {\n  name: string;\n  value: number;\n  color: string;\n  /** What this slice's value is made of, e.g. cost basis vs. gain. Shown as a\n   * small segmented bar and a value per part in the hover tooltip; omit when\n   * there's nothing to break the slice down into. */\n  breakdown?: DonutBreakdownPart[];\n}\n\ninterface Props {\n  data: DonutSlice[];\n  size: number;\n  thickness?: number;\n  formatValue: (n: number) => string;\n  /** Centre readout when nothing is hovered, e.g. the total. */\n  centerLabel: string;\n  centerValue: ReactNode;\n  /** Highlight a slice from outside (e.g. the table row being hovered). When\n   * given, it takes over from the donut's own hover state. */\n  active?: string | null;\n  /** Called when the pointer or focus moves onto / off a slice. */\n  onActiveChange?: (name: string | null) => void;\n}\n\n/** A thin bar split by explicit parts (not auto-assigned identity colours,\n * unlike WeightBar) — used for a value's own composition, e.g. principal vs\n * gain, where colour means direction rather than \"which entity\". */\nfunction CompositionBar({ parts, total }: { parts: DonutBreakdownPart[]; total: number }) {\n  const shown = parts.filter((p) => p.value !== 0);\n  if (shown.length === 0 || total <= 0) return null;\n  return (\n    <div className=\"flex h-1.5 w-full gap-[2px] overflow-hidden rounded-full\" role=\"img\" aria-hidden=\"true\">\n      {shown.map((p) => (\n        <div\n          key={p.key}\n          className=\"h-full first:rounded-l-full last:rounded-r-full\"\n          style={{ width: `${(Math.abs(p.value) / total) * 100}%`, backgroundColor: `hsl(var(${p.colorVar}))` }}\n        />\n      ))}\n    </div>\n  );\n}\n\n/**\n * A donut whose centre is the readout: hovering or focusing a slice swaps the\n * centre from the total to that slice's name, value and share, no floating\n * tooltip covering the ring. The active slice lifts slightly; the rest fade.\n *\n * A slice with `breakdown` also gets a floating tooltip near the pointer\n * (the ring is curved, so a fixed-position readout can't sit \"on\" a slice the\n * way a bar or bin can) showing what the value is made of: a small segmented\n * bar plus one row per part.\n */\nexport function DonutChart({ data, size, thickness = 30, formatValue, centerLabel, centerValue, active: activeProp, onActiveChange }: Props) {\n  const [internalActive, setInternalActive] = useState<string | null>(null);\n  const active = activeProp !== undefined ? activeProp : internalActive;\n  const setActive = (name: string | null) => {\n    setInternalActive(name);\n    onActiveChange?.(name);\n  };\n  const [pointer, setPointer] = useState<{ x: number; y: number } | null>(null);\n  const radius = size / 2;\n  const total = data.reduce((s, d) => s + d.value, 0);\n  const hovered = data.find((d) => d.name === active);\n  // The centre readout must fit inside the ring's hole, not a fixed guess at\n  // padding: a smaller `size` (this component is used at everything from 140\n  // to 200+) or a thicker ring shrinks the hole, and a fixed px-10 either\n  // wasted space or let long text spill out past the ring with no guard.\n  const innerDiameter = Math.max(0, 2 * (radius - 4 - thickness));\n  const textBoxSize = innerDiameter * 0.86;\n\n  const track = (e: PointerEvent<SVGPathElement>) => {\n    const pt = localPoint(e);\n    if (pt) setPointer({ x: pt.x, y: pt.y });\n  };\n\n  return (\n    <div className=\"relative\" style={{ width: size, height: size }}>\n      <svg width={size} height={size} role=\"group\" aria-label={`${centerLabel}: ${data.length} slices`}>\n        <Group top={radius} left={radius}>\n          <Pie\n            data={data}\n            pieValue={(d) => d.value}\n            pieSort={null}\n            outerRadius={radius - 4}\n            innerRadius={radius - 4 - thickness}\n            padAngle={0.02}\n            cornerRadius={4}\n          >\n            {(pie) =>\n              pie.arcs.map((arc) => {\n                const isActive = active === arc.data.name;\n                const pct = total > 0 ? (arc.data.value / total) * 100 : 0;\n                return (\n                  <path\n                    key={arc.data.name}\n                    d={pie.path(arc) ?? \"\"}\n                    fill={arc.data.color}\n                    opacity={active && !isActive ? 0.35 : 1}\n                    transform={isActive ? \"scale(1.04)\" : undefined}\n                    tabIndex={0}\n                    role=\"img\"\n                    aria-label={`${arc.data.name}: ${formatValue(arc.data.value)}, ${pct.toFixed(1)}%${\n                      arc.data.breakdown ? \", \" + arc.data.breakdown.map((p) => `${p.label} ${formatValue(p.value)}`).join(\", \") : \"\"\n                    }`}\n                    className=\"outline-none cursor-pointer\"\n                    style={{ transition: \"opacity 150ms ease, transform 150ms ease\" }}\n                    onPointerEnter={(e) => { setActive(arc.data.name); track(e); }}\n                    onPointerMove={track}\n                    onPointerLeave={() => { setActive(null); setPointer(null); }}\n                    onFocus={() => setActive(arc.data.name)}\n                    onBlur={() => setActive(null)}\n                  />\n                );\n              })\n            }\n          </Pie>\n        </Group>\n      </svg>\n      <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\">\n        <div className=\"flex flex-col items-center\" style={{ width: textBoxSize, maxWidth: textBoxSize }}>\n          <span className=\"text-[10px] uppercase tracking-wider text-muted-foreground truncate max-w-full\">\n            {hovered ? hovered.name : centerLabel}\n          </span>\n          <span className=\"text-sm font-semibold tabular-nums text-foreground truncate max-w-full\">\n            {hovered ? formatValue(hovered.value) : centerValue}\n          </span>\n          {hovered && total > 0 && (\n            <span className=\"text-[10px] tabular-nums text-muted-foreground\">\n              {((hovered.value / total) * 100).toFixed(1)}%\n            </span>\n          )}\n        </div>\n      </div>\n      {hovered?.breakdown && pointer && (() => {\n        // The parts' own sum is the bar's 100%, not the slice's current\n        // value: a losing holding's parts are \"current value\" + \"loss\",\n        // which together equal what was originally invested, a larger\n        // number than the now-smaller current value.\n        const partsTotal = hovered.breakdown.reduce((s, p) => s + Math.abs(p.value), 0);\n        return (\n          <ChartTooltip x={pointer.x} y={pointer.y} containerWidth={size}>\n            <p className=\"font-semibold text-foreground\">{hovered.name}</p>\n            <CompositionBar parts={hovered.breakdown} total={partsTotal} />\n            {hovered.breakdown.map((p) => (\n              <TooltipRow\n                key={p.key}\n                color={`hsl(var(${p.colorVar}))`}\n                label={p.label}\n                value={`${p.value < 0 ? \"-\" : \"\"}${formatValue(Math.abs(p.value))}${\n                  partsTotal > 0 ? ` · ${((Math.abs(p.value) / partsTotal) * 100).toFixed(0)}%` : \"\"\n                }`}\n              />\n            ))}\n          </ChartTooltip>\n        );\n      })()}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/charts/GroupedBarChart.tsx",
      "type": "text/plain",
      "content": "import { useMemo, useState, type ReactNode } from \"react\";\nimport { BarRounded } from \"@visx/shape\";\nimport { AxisBottom, AxisLeft } from \"@visx/axis\";\nimport { GridRows } from \"@visx/grid\";\nimport { Group } from \"@visx/group\";\nimport { scaleBand, scaleLinear } from \"@visx/scale\";\nimport { useParentSize } from \"@visx/responsive\";\nimport { ChartTooltip } from \"./ChartTooltip\";\n\nexport interface BarSeries<K extends string> {\n  key: K;\n  label: string;\n  /** Name of an HSL colour token, e.g. `--series-income`. */\n  colorVar: string;\n}\n\ninterface Props<T, K extends string> {\n  data: T[];\n  series: BarSeries<K>[];\n  /** Category label for each datum (x axis). */\n  getLabel: (d: T) => string;\n  getValue: (d: T, key: K) => number;\n  height: number;\n  formatAxisValue: (n: number) => string;\n  renderTooltip: (d: T) => ReactNode;\n  yAxisWidth?: number;\n}\n\nconst MARGIN = { top: 8, right: 8, bottom: 28 };\nconst MAX_BAR = 22;\nconst BAR_GAP = 2;\nconst axisText = { fill: \"hsl(var(--muted-foreground))\", fontSize: 10 } as const;\n\n/**\n * Side-by-side columns per category (income vs expenses per month). Columns\n * are capped at 22px with a 2px gap inside each pair, rounded only at the\n * data end, and the whole category band is the hover target.\n */\nexport function GroupedBarChart<T, K extends string>({\n  data,\n  series,\n  getLabel,\n  getValue,\n  height,\n  formatAxisValue,\n  renderTooltip,\n  yAxisWidth = 52,\n}: Props<T, K>) {\n  const { parentRef, width } = useParentSize({ debounceTime: 30 });\n  const [active, setActive] = useState<number | null>(null);\n  const innerW = Math.max(0, width - yAxisWidth - MARGIN.right);\n  const innerH = Math.max(0, height - MARGIN.top - MARGIN.bottom);\n\n  const xScale = useMemo(\n    () => scaleBand({ domain: data.map((_, i) => i), range: [0, innerW], paddingInner: 0.35, paddingOuter: 0.2 }),\n    [data, innerW]\n  );\n  const yScale = useMemo(() => {\n    const max = Math.max(0, ...data.flatMap((d) => series.map((s) => getValue(d, s.key))));\n    return scaleLinear({ domain: [0, max || 1], range: [innerH, 0], nice: 4 });\n  }, [data, series, getValue, innerH]);\n\n  const band = xScale.bandwidth();\n  const barW = Math.max(1, Math.min(MAX_BAR, (band - BAR_GAP * (series.length - 1)) / series.length));\n  const groupW = barW * series.length + BAR_GAP * (series.length - 1);\n\n  return (\n    <div ref={parentRef} className=\"relative w-full\" style={{ height }}>\n      {width > 0 && (\n        <svg width={width} height={height}>\n          <Group left={yAxisWidth} top={MARGIN.top}>\n            <GridRows scale={yScale} width={innerW} numTicks={4} stroke=\"hsl(var(--border))\" strokeOpacity={0.7} />\n            {data.map((d, i) => {\n              const bx = xScale(i) ?? 0;\n              const gx = bx + (band - groupW) / 2;\n              return (\n                <g key={i}>\n                  {active === i && (\n                    <rect x={bx - band * 0.15} y={0} width={band * 1.3} height={innerH} rx={4} fill=\"hsl(var(--muted))\" opacity={0.5} />\n                  )}\n                  {series.map((s, si) => {\n                    const v = getValue(d, s.key);\n                    const top = yScale(v);\n                    const h = innerH - top;\n                    if (h <= 0) return null;\n                    return (\n                      <BarRounded\n                        key={s.key}\n                        x={gx + si * (barW + BAR_GAP)}\n                        y={top}\n                        width={barW}\n                        height={h}\n                        radius={Math.min(4, h)}\n                        top\n                        fill={`hsl(var(${s.colorVar}))`}\n                      />\n                    );\n                  })}\n                  <rect\n                    x={bx - band * 0.15}\n                    y={0}\n                    width={band * 1.3}\n                    height={innerH}\n                    fill=\"transparent\"\n                    tabIndex={0}\n                    role=\"img\"\n                    aria-label={`${getLabel(d)}: ${series.map((s) => `${s.label} ${formatAxisValue(getValue(d, s.key))}`).join(\", \")}`}\n                    className=\"outline-none\"\n                    onPointerEnter={() => setActive(i)}\n                    onPointerLeave={() => setActive(null)}\n                    onFocus={() => setActive(i)}\n                    onBlur={() => setActive(null)}\n                  />\n                </g>\n              );\n            })}\n            <AxisLeft\n              scale={yScale}\n              numTicks={4}\n              hideAxisLine\n              hideTicks\n              tickFormat={(v) => formatAxisValue(Number(v))}\n              tickLabelProps={() => ({ ...axisText, textAnchor: \"end\", dx: -6, dy: 3 })}\n            />\n            <AxisBottom\n              top={innerH}\n              scale={xScale}\n              hideAxisLine\n              hideTicks\n              tickFormat={(i) => getLabel(data[Number(i)])}\n              tickLabelProps={() => ({ ...axisText, textAnchor: \"middle\", dy: 4 })}\n            />\n          </Group>\n        </svg>\n      )}\n      {active !== null && data[active] && (\n        <ChartTooltip\n          x={yAxisWidth + (xScale(active) ?? 0) + band}\n          y={MARGIN.top + innerH / 3}\n          containerWidth={width}\n        >\n          {renderTooltip(data[active])}\n        </ChartTooltip>\n      )}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/charts/HorizontalBarChart.tsx",
      "type": "text/plain",
      "content": "import { useMemo, useState, type ReactNode } from \"react\";\nimport { BarRounded } from \"@visx/shape\";\nimport { Group } from \"@visx/group\";\nimport { scaleBand, scaleLinear } from \"@visx/scale\";\nimport { useParentSize } from \"@visx/responsive\";\nimport { ChartTooltip } from \"./ChartTooltip\";\n\ninterface Props<T> {\n  data: T[];\n  getLabel: (d: T) => string;\n  getValue: (d: T) => number;\n  /** Name of an HSL colour token for the bars. */\n  colorVar: string;\n  /** Direct label at each bar's tip. */\n  formatValue: (n: number) => string;\n  renderTooltip: (d: T) => ReactNode;\n  onSelect?: (d: T) => void;\n  /** Label of the selected row; the others fade back. */\n  selected?: string | null;\n  labelWidth?: number;\n  /** Rows shrink (down to 22px) so a long tail fits inside this height. */\n  maxHeight?: number;\n}\n\nconst ROW = 34;\nconst MIN_ROW = 22;\nconst MAX_BAR = 20;\nconst VALUE_GUTTER = 64;\n\nfunction truncate(s: string, max: number) {\n  return s.length > max ? `${s.slice(0, max - 1)}…` : s;\n}\n\n/**\n * Sorted horizontal bars with the category on the left and the value at the\n * tip. One hue (length already encodes magnitude), so opacity is free to\n * carry selection. Rows are keyboard-focusable and Enter selects.\n */\nexport function HorizontalBarChart<T>({\n  data,\n  getLabel,\n  getValue,\n  colorVar,\n  formatValue,\n  renderTooltip,\n  onSelect,\n  selected,\n  labelWidth = 132,\n  maxHeight = 520,\n}: Props<T>) {\n  const { parentRef, width } = useParentSize({ debounceTime: 30 });\n  const [active, setActive] = useState<number | null>(null);\n  const row = Math.max(MIN_ROW, Math.min(ROW, (maxHeight - 8) / Math.max(1, data.length)));\n  const height = Math.max(180, data.length * row + 8);\n  const innerW = Math.max(0, width - labelWidth - VALUE_GUTTER);\n\n  const yScale = useMemo(\n    () => scaleBand({ domain: data.map((_, i) => i), range: [4, height - 4], paddingInner: 0.3 }),\n    [data, height]\n  );\n  const xScale = useMemo(\n    () => scaleLinear({ domain: [0, Math.max(0, ...data.map(getValue)) || 1], range: [0, innerW] }),\n    [data, getValue, innerW]\n  );\n  const barH = Math.min(MAX_BAR, yScale.bandwidth());\n  // ~6.6px per character at 11px, less the 10px gap before the bar.\n  const maxChars = Math.floor((labelWidth - 12) / 6.6);\n\n  return (\n    <div ref={parentRef} className=\"relative w-full\" style={{ height }}>\n      {width > 0 && (\n        <svg width={width} height={height}>\n          {data.map((d, i) => {\n            const label = getLabel(d);\n            const v = getValue(d);\n            const rowY = yScale(i) ?? 0;\n            const cy = rowY + yScale.bandwidth() / 2;\n            const w = Math.max(v > 0 ? 2 : 0, xScale(v));\n            const faded = !!selected && selected !== label;\n            return (\n              <g\n                key={label}\n                tabIndex={0}\n                role={onSelect ? \"button\" : \"img\"}\n                aria-label={`${label}: ${formatValue(v)}`}\n                aria-pressed={onSelect ? selected === label : undefined}\n                className=\"outline-none [&:focus-visible>rect:first-child]:fill-muted\"\n                style={{ cursor: onSelect ? \"pointer\" : undefined }}\n                onPointerEnter={() => setActive(i)}\n                onPointerLeave={() => setActive(null)}\n                onFocus={() => setActive(i)}\n                onBlur={() => setActive(null)}\n                onClick={() => onSelect?.(d)}\n                onKeyDown={(e) => {\n                  if (onSelect && (e.key === \"Enter\" || e.key === \" \")) {\n                    e.preventDefault();\n                    onSelect(d);\n                  }\n                }}\n              >\n                {/* Full-row hit target, washed on hover. */}\n                <rect\n                  x={0}\n                  y={rowY - 3}\n                  width={width}\n                  height={yScale.bandwidth() + 6}\n                  rx={4}\n                  fill={active === i ? \"hsl(var(--muted))\" : \"transparent\"}\n                  opacity={0.5}\n                />\n                <text x={labelWidth - 10} y={cy} dy=\"0.33em\" textAnchor=\"end\" fontSize={11} fill=\"hsl(var(--muted-foreground))\">\n                  <title>{label}</title>\n                  {truncate(label, maxChars)}\n                </text>\n                <Group left={labelWidth}>\n                  {w > 0 && (\n                    <BarRounded\n                      x={0}\n                      y={cy - barH / 2}\n                      width={w}\n                      height={barH}\n                      radius={Math.min(4, w)}\n                      right\n                      fill={`hsl(var(${colorVar}))`}\n                      fillOpacity={faded ? 0.3 : 0.85}\n                    />\n                  )}\n                  <text x={w + 8} y={cy} dy=\"0.33em\" fontSize={11} fill=\"hsl(var(--foreground))\" className=\"tabular-nums\">\n                    {formatValue(v)}\n                  </text>\n                </Group>\n              </g>\n            );\n          })}\n        </svg>\n      )}\n      {active !== null && data[active] && (\n        <ChartTooltip\n          x={labelWidth + xScale(getValue(data[active]))}\n          y={(yScale(active) ?? 0) + yScale.bandwidth()}\n          containerWidth={width}\n        >\n          {renderTooltip(data[active])}\n        </ChartTooltip>\n      )}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/charts/SegmentedControl.tsx",
      "type": "text/plain",
      "content": "import { cn } from \"@/lib/utils\";\n\n/**\n * The period switcher (30 days / 3 months / …) that sat hand-rolled above each\n * time-series chart. Same look, now shared, and exposed as a radio group so\n * screen readers announce the selected range.\n */\nexport function SegmentedControl<T extends string>({\n  options,\n  value,\n  onChange,\n  label,\n  className,\n}: {\n  options: readonly { value: T; label: string }[];\n  value: T;\n  onChange: (value: NoInfer<T>) => void;\n  label: string;\n  className?: string;\n}) {\n  return (\n    <div role=\"radiogroup\" aria-label={label} className={cn(\"flex items-center gap-0.5\", className)}>\n      {options.map((o) => (\n        <button\n          key={o.value}\n          type=\"button\"\n          role=\"radio\"\n          aria-checked={value === o.value}\n          tabIndex={value === o.value ? 0 : -1}\n          onClick={() => onChange(o.value)}\n          onKeyDown={(event) => {\n            const index = options.findIndex(option => option.value === o.value);\n            const next = event.key === \"ArrowRight\" || event.key === \"ArrowDown\" ? (index + 1) % options.length\n              : event.key === \"ArrowLeft\" || event.key === \"ArrowUp\" ? (index - 1 + options.length) % options.length\n              : event.key === \"Home\" ? 0 : event.key === \"End\" ? options.length - 1 : null;\n            if (next === null) return;\n            event.preventDefault();\n            onChange(options[next].value);\n            const buttons = event.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>('[role=\"radio\"]');\n            buttons?.[next]?.focus();\n          }}\n          className={cn(\n            \"px-2.5 py-1 text-xs font-medium rounded-md transition-colors\",\n            value === o.value\n              ? \"bg-muted text-foreground\"\n              : \"text-muted-foreground hover:text-foreground hover:bg-muted/50\"\n          )}\n        >\n          {o.label}\n        </button>\n      ))}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/charts/TimeSeriesChart.tsx",
      "type": "text/plain",
      "content": "import { useCallback, useId, useMemo, useState, type KeyboardEvent, type PointerEvent } from \"react\";\nimport { AreaClosed, Line, LinePath } from \"@visx/shape\";\nimport { AxisBottom, AxisLeft } from \"@visx/axis\";\nimport { GridRows } from \"@visx/grid\";\nimport { Group } from \"@visx/group\";\nimport { scaleLinear, scaleTime } from \"@visx/scale\";\nimport { curveMonotoneX } from \"@visx/curve\";\nimport { localPoint } from \"@visx/event\";\nimport { useParentSize } from \"@visx/responsive\";\nimport { bisector, extent } from \"d3-array\";\nimport { parseLocalDate } from \"@/lib/utils\";\nimport { ChartTooltip, TooltipRow } from \"./ChartTooltip\";\n\nexport interface TimePoint {\n  /** Local calendar date, `YYYY-MM-DD`. */\n  date: string;\n  value: number;\n}\n\ninterface Props {\n  data: TimePoint[];\n  height: number;\n  /** Name of an HSL colour token, e.g. `--chart-blue`. */\n  colorVar?: string;\n  /** Series name shown in the tooltip, e.g. \"Balance\". */\n  label: string;\n  formatValue: (n: number) => string;\n  formatAxisValue?: (n: number) => string;\n  /** Tooltip date, so it honours the user's date-format setting. */\n  formatDate?: (date: string) => string;\n  /** Draw a zero baseline (with this caption) whenever the series dips below 0. */\n  zeroLineLabel?: string;\n  yAxisWidth?: number;\n  /** Daily flow amounts need a true zero baseline even in line mode. */\n  zeroBaseline?: boolean;\n  /** \"line\" (default): area + line, the original look. \"bar\": one column per\n   * point, sharing the same scales/axes/tooltip/keyboard nav. */\n  chartType?: \"line\" | \"bar\";\n}\n\nconst MARGIN = { top: 8, right: 8, bottom: 28 };\nconst axisText = { fill: \"hsl(var(--muted-foreground))\", fontSize: 10 } as const;\ntype P = { t: Date; v: number; date: string };\nconst byDate = bisector<P, Date>((d) => d.t).center;\n\n/**\n * Area + line over time with a snapping crosshair, the chart behind every\n * balance and price trend. Monotone curve rather than Recharts' \"natural\":\n * natural overshoots between points, drawing balances that never existed.\n *\n * Arrow keys walk the points when the chart has focus, so the readout is not\n * pointer-only.\n */\nexport function TimeSeriesChart({\n  data,\n  height,\n  colorVar = \"--primary\",\n  label,\n  formatValue,\n  formatAxisValue = formatValue,\n  formatDate = (d) => parseLocalDate(d).toLocaleDateString(undefined, { month: \"short\", day: \"numeric\", year: \"numeric\" }),\n  zeroLineLabel,\n  yAxisWidth = 48,\n  zeroBaseline = false,\n  chartType = \"line\",\n}: Props) {\n  const { parentRef, width } = useParentSize({ debounceTime: 30 });\n  const gradientId = useId();\n  const [active, setActive] = useState<number | null>(null);\n  const color = `hsl(var(${colorVar}))`;\n\n  const points = useMemo<P[]>(\n    () =>\n      data\n        .map((d) => ({ t: parseLocalDate(d.date), v: d.value, date: d.date }))\n        // A row with an unreadable date can't be placed on a time axis.\n        .filter((p) => !Number.isNaN(p.t.getTime()) && Number.isFinite(p.v)),\n    [data]\n  );\n  const innerW = Math.max(0, width - yAxisWidth - MARGIN.right);\n  const innerH = Math.max(0, height - MARGIN.top - MARGIN.bottom);\n  const dipsBelowZero = !!zeroLineLabel && points.some((p) => p.v < 0);\n\n  const xScale = useMemo(() => {\n    const [lo, hi] = extent(points, (p) => p.t) as [Date, Date];\n    return scaleTime({ domain: [lo ?? new Date(), hi ?? new Date()], range: [0, innerW] });\n  }, [points, innerW]);\n\n  const yScale = useMemo(() => {\n    let [lo, hi] = extent(points, (p) => p.v) as [number, number];\n    lo ??= 0;\n    hi ??= 0;\n    if (dipsBelowZero) hi = Math.max(hi, 0);\n    // A bar's height only reads correctly against a true zero baseline (two\n    // bars twice as tall as each other should mean twice the value); the\n    // line chart has no such requirement and keeps the tighter, padded-to-\n    // the-data domain that makes small movements visible.\n    if (chartType === \"bar\" || zeroBaseline) lo = Math.min(lo, 0);\n    const topPad = (hi - lo) * 0.08 || Math.abs(hi) * 0.08 || 1;\n    const bottomPad = chartType === \"bar\" || zeroBaseline ? 0 : topPad;\n    return scaleLinear({ domain: [lo - bottomPad, hi + topPad], range: [innerH, 0], nice: 4 });\n  }, [points, innerH, dipsBelowZero, chartType, zeroBaseline]);\n\n  const spanDays = points.length > 1 ? (points[points.length - 1].t.getTime() - points[0].t.getTime()) / 86_400_000 : 0;\n  const tickDate = (d: Date) =>\n    d.toLocaleDateString(undefined, spanDays > 300 ? { month: \"short\", year: \"2-digit\" } : { month: \"short\", day: \"numeric\" });\n\n  const onPointerMove = useCallback(\n    (e: PointerEvent<SVGRectElement>) => {\n      const pt = localPoint(e);\n      if (!pt || points.length === 0) return;\n      setActive(byDate(points, xScale.invert(pt.x - yAxisWidth)));\n    },\n    [points, xScale, yAxisWidth]\n  );\n\n  const onKeyDown = (e: KeyboardEvent<SVGSVGElement>) => {\n    if (points.length === 0) return;\n    const last = points.length - 1;\n    const next =\n      e.key === \"ArrowLeft\" ? Math.max(0, (active ?? last) - 1)\n      : e.key === \"ArrowRight\" ? Math.min(last, (active ?? last) + 1)\n      : e.key === \"Home\" ? 0\n      : e.key === \"End\" ? last\n      : null;\n    if (next === null) return;\n    e.preventDefault();\n    setActive(next);\n  };\n\n  const shown = active !== null ? points[active] : null;\n  const lastPoint = points[points.length - 1];\n  const x = (p: P) => xScale(p.t);\n  const y = (p: P) => yScale(p.v);\n\n  // Bar width from the tightest gap between consecutive points, so daily\n  // data doesn't touch and sparse data doesn't turn into hairlines.\n  const barWidth = useMemo(() => {\n    if (points.length < 2) return Math.min(24, innerW * 0.5);\n    const xs = points.map((p) => xScale(p.t)).sort((a, b) => a - b);\n    let minGap = Infinity;\n    for (let i = 1; i < xs.length; i++) minGap = Math.min(minGap, xs[i] - xs[i - 1]);\n    return Math.max(2, Math.min(28, minGap * 0.6));\n  }, [points, xScale, innerW]);\n\n  return (\n    <div ref={parentRef} className=\"relative w-full\" style={{ height }}>\n      {width > 0 && points.length > 0 && (\n        <svg\n          width={width}\n          height={height}\n          role=\"img\"\n          tabIndex={0}\n          aria-label={`${label}: ${formatValue(points[0].v)} on ${formatDate(points[0].date)} to ${formatValue(lastPoint.v)} on ${formatDate(lastPoint.date)}. Use arrow keys to read values.`}\n          onKeyDown={onKeyDown}\n          onFocus={() => setActive((a) => a ?? points.length - 1)}\n          onBlur={() => setActive(null)}\n          className=\"outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm\"\n        >\n          <defs>\n            <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n              <stop offset=\"0%\" stopColor={color} stopOpacity={0.14} />\n              <stop offset=\"100%\" stopColor={color} stopOpacity={0} />\n            </linearGradient>\n          </defs>\n          <Group left={yAxisWidth} top={MARGIN.top}>\n            <GridRows scale={yScale} width={innerW} numTicks={4} stroke=\"hsl(var(--border))\" strokeOpacity={0.7} />\n            {dipsBelowZero && (\n              <>\n                <Line from={{ x: 0, y: yScale(0) }} to={{ x: innerW, y: yScale(0) }} stroke=\"hsl(var(--chart-red))\" strokeOpacity={0.5} />\n                <text x={4} y={yScale(0) - 4} fontSize={10} fill=\"hsl(var(--muted-foreground))\">{zeroLineLabel}</text>\n              </>\n            )}\n            {chartType === \"bar\" ? (\n              points.map((p, i) => {\n                const top = y(p);\n                // Bars grow from the true zero line (the yScale domain\n                // itself extends to 0 in bar mode, see above), so height is\n                // always proportional to value, not just to the visible range.\n                return (\n                  <rect\n                    key={p.date}\n                    x={x(p) - barWidth / 2}\n                    y={top}\n                    width={barWidth}\n                    height={Math.max(1, innerH - top)}\n                    rx={Math.min(2, barWidth / 2)}\n                    fill={color}\n                    opacity={active !== null && active !== i ? 0.4 : 0.85}\n                    style={{ transition: \"opacity 150ms ease\" }}\n                  />\n                );\n              })\n            ) : (\n              <>\n                <AreaClosed data={points} x={x} y={y} yScale={yScale} curve={curveMonotoneX} fill={`url(#${gradientId})`} />\n                <LinePath data={points} x={x} y={y} curve={curveMonotoneX} stroke={color} strokeWidth={2} strokeLinejoin=\"round\" strokeLinecap=\"round\" />\n              </>\n            )}\n            <AxisLeft\n              scale={yScale}\n              numTicks={4}\n              hideAxisLine\n              hideTicks\n              tickFormat={(v) => formatAxisValue(Number(v))}\n              tickLabelProps={() => ({ ...axisText, textAnchor: \"end\", dx: -6, dy: 3 })}\n            />\n            <AxisBottom\n              top={innerH}\n              scale={xScale}\n              numTicks={Math.max(2, Math.min(6, Math.floor(innerW / 90)))}\n              hideAxisLine\n              hideTicks\n              tickFormat={(v) => tickDate(v as Date)}\n              tickLabelProps={() => ({ ...axisText, textAnchor: \"middle\", dy: 4 })}\n            />\n            {/* Bar mode signals the active point through the bar's own\n                opacity above; a dot sitting on top of a column, or a\n                crosshair cutting through it, would be redundant. */}\n            {chartType === \"line\" && (\n              shown ? (\n                <g pointerEvents=\"none\">\n                  <Line from={{ x: x(shown), y: 0 }} to={{ x: x(shown), y: innerH }} stroke=\"hsl(var(--muted-foreground))\" strokeOpacity={0.35} />\n                  <circle cx={x(shown)} cy={y(shown)} r={4.5} fill={color} stroke=\"hsl(var(--card))\" strokeWidth={2} />\n                </g>\n              ) : (\n                // Resting end-dot marks \"today\" on the line.\n                <circle cx={x(lastPoint)} cy={y(lastPoint)} r={4} fill={color} stroke=\"hsl(var(--card))\" strokeWidth={2} pointerEvents=\"none\" />\n              )\n            )}\n          </Group>\n          {/* Hit area covers the whole plot: readers aim at a date, not a 2px line. */}\n          <rect\n            x={yAxisWidth}\n            y={MARGIN.top}\n            width={innerW}\n            height={innerH}\n            fill=\"transparent\"\n            onPointerMove={onPointerMove}\n            onPointerLeave={() => setActive(null)}\n          />\n        </svg>\n      )}\n      {shown && (\n        <ChartTooltip x={yAxisWidth + x(shown)} y={MARGIN.top + y(shown)} containerWidth={width}>\n          <p className=\"font-semibold text-foreground\">{formatDate(shown.date)}</p>\n          <TooltipRow color={color} mark=\"line\" label={label} value={formatValue(shown.v)} />\n        </ChartTooltip>\n      )}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/charts/WeightRing.tsx",
      "type": "text/plain",
      "content": "/**\n * A tiny circular progress ring showing one row's share of a total, for a\n * table where a bar would take a whole column but a ring fits inline next to\n * the label it belongs to.\n */\nexport function WeightRing({ pct, size = 22, thickness = 3 }: { pct: number; size?: number; thickness?: number }) {\n  const clamped = Math.max(0, Math.min(100, pct));\n  const radius = (size - thickness) / 2;\n  const circumference = 2 * Math.PI * radius;\n  const offset = circumference * (1 - clamped / 100);\n  return (\n    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className=\"shrink-0\" role=\"img\" aria-label={`${clamped.toFixed(1)}% of portfolio`}>\n      <circle cx={size / 2} cy={size / 2} r={radius} fill=\"none\" stroke=\"hsl(var(--border))\" strokeWidth={thickness} />\n      <circle\n        cx={size / 2}\n        cy={size / 2}\n        r={radius}\n        fill=\"none\"\n        stroke=\"hsl(var(--primary))\"\n        strokeWidth={thickness}\n        strokeLinecap=\"round\"\n        strokeDasharray={circumference}\n        strokeDashoffset={offset}\n        transform={`rotate(-90 ${size / 2} ${size / 2})`}\n      />\n    </svg>\n  );\n}\n"
    },
    {
      "name": "src/components/commandPaletteConfig.ts",
      "type": "text/plain",
      "content": "import { DASHBOARD_PATH } from \"@/lib/deployment\";\nimport type { ComponentType } from \"react\";\nimport { ArrowLeftRight, BarChart3, FileBarChart, Flag, Home, Landmark, Lightbulb, Settings, Target, TrendingUp, Wallet } from \"lucide-react\";\n\nexport const PAGES: { label: string; to: string; keys: string; icon: ComponentType<{ className?: string }> }[] = [\n  { label: \"Home\", to: DASHBOARD_PATH, keys: \"g h\", icon: Home },\n  { label: \"Transactions\", to: \"/transactions\", keys: \"g t\", icon: ArrowLeftRight },\n  { label: \"Analytics\", to: \"/analytics\", keys: \"g a\", icon: BarChart3 },\n  { label: \"Reports\", to: \"/reports\", keys: \"g r\", icon: FileBarChart },\n  { label: \"Budgets\", to: \"/budgets\", keys: \"g b\", icon: Target },\n  { label: \"Investments\", to: \"/investments\", keys: \"g i\", icon: TrendingUp },\n  { label: \"Accounts\", to: \"/accounts\", keys: \"g c\", icon: Landmark },\n  { label: \"Assets\", to: \"/assets\", keys: \"g e\", icon: Wallet },\n  { label: \"Financial planning\", to: \"/planning\", keys: \"g p\", icon: Lightbulb },\n  { label: \"Goals\", to: \"/goals\", keys: \"g g\", icon: Flag },\n  { label: \"Settings\", to: \"/settings\", keys: \"g s\", icon: Settings },\n];\n\n/** Other components open the palette by dispatching this (e.g. the top-bar button). */\nexport const OPEN_PALETTE_EVENT = \"open-command-palette\";\n"
    },
    {
      "name": "src/components/dashboard/BalanceChart.tsx",
      "type": "text/plain",
      "content": "import { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { TimeSeriesChart } from \"@/components/charts/TimeSeriesChart\";\nimport { SegmentedControl } from \"@/components/charts/SegmentedControl\";\nimport { ChartTypeToggle } from \"@/components/charts/ChartTypeToggle\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { periodChange } from \"@/lib/balanceSheet\";\nimport { cashBalanceSeries } from \"@/lib/balanceHistory\";\nimport { useParentSize } from \"@visx/responsive\";\nimport { TrendingDown, TrendingUp, LineChart as LineChartIcon, BarChart3 } from \"lucide-react\";\n\nconst CHART_TYPES = [\n  { value: \"line\", icon: LineChartIcon, label: \"Line\" },\n  { value: \"bar\", icon: BarChart3, label: \"Bar\" },\n] as const;\n\nconst periods = [\n  { label: \"30 days\", days: 30 },\n  { label: \"3 months\", days: 90 },\n  { label: \"6 months\", days: 180 },\n  { label: \"All time\", days: 999 },\n];\nconst periodOptions = periods.map((p) => ({ value: p.label, label: p.label }));\n\ninterface BalanceChartProps {\n  inline?: boolean;\n}\n\nexport function BalanceChart({ inline }: BalanceChartProps) {\n  const { transactions, accounts } = useFinanceData({ summary: true });\n  const [period, setPeriod] = useState(\"3 months\");\n  const [chartType, setChartType] = useState<\"line\" | \"bar\">(\"line\");\n  const { compact, money, date: fmtDate } = useMoney();\n\n  // Spendable cash (checking + savings) over time: see cashBalanceSeries for\n  // why cars, loans and brokerage accounts are left out and how the line is\n  // smoothed into a trend instead of a heartbeat.\n  const days = periods.find((p) => p.label === period)?.days ?? 90;\n  const data = useMemo(\n    () => cashBalanceSeries(transactions, accounts, days >= 999 ? null : days),\n    [transactions, accounts, days]\n  );\n\n  const change = useMemo(() => periodChange(data.map((d) => d.value)), [data]);\n  const { parentRef: fillRef, height: fillHeight } = useParentSize({ debounceTime: 30 });\n\n  const chart = (\n    <div className={cn(inline && \"flex flex-col h-full\")}>\n      <div className=\"flex flex-wrap items-center justify-between gap-2 mb-2\">\n        {change ? (\n          <p className=\"flex items-center gap-1 text-xs tabular-nums\">\n            {change.amount > 0 ? (\n              <TrendingUp size={14} strokeWidth={1.75} className=\"text-chart-green\" aria-hidden />\n            ) : change.amount < 0 ? (\n              <TrendingDown size={14} strokeWidth={1.75} className=\"text-chart-red\" aria-hidden />\n            ) : null}\n            <span className={cn(\"font-medium\", change.amount > 0 ? \"text-chart-green\" : change.amount < 0 ? \"text-chart-red\" : \"text-foreground\")}>\n              {change.amount > 0 ? \"+\" : change.amount < 0 ? \"-\" : \"\"}{money(Math.abs(change.amount))}\n              {change.pct !== null && ` (${change.pct > 0 ? \"+\" : \"\"}${change.pct.toFixed(1)}%)`}\n            </span>\n            <span className=\"sr-only\">{period === \"All time\" ? \"all time\" : `over ${period}`}</span>\n          </p>\n        ) : (\n          <span />\n        )}\n        <div className=\"flex items-center gap-2 ml-auto\">\n          <ChartTypeToggle options={CHART_TYPES} value={chartType} onChange={setChartType} label=\"Chart type\" />\n          <SegmentedControl label=\"Chart period\" options={periodOptions} value={period} onChange={setPeriod} />\n        </div>\n      </div>\n      {/* Inline, the plot grows to fill its card when the card beside it is\n          taller, instead of leaving a blank band under the chart. */}\n      <div ref={inline ? fillRef : undefined} className={cn(inline && \"flex-1 min-h-[260px]\")}>\n        {data.length ? <TimeSeriesChart\n          data={data}\n          height={inline ? Math.max(260, Math.floor(fillHeight)) : 280}\n          label=\"Balance\"\n          formatValue={money}\n          formatAxisValue={compact}\n          formatDate={(d) => fmtDate(d)}\n          chartType={chartType}\n        /> : <p className=\"py-12 text-center text-sm text-muted-foreground\">No transactions to chart for checking and savings.</p>}\n      </div>\n    </div>\n  );\n\n  if (inline) return chart;\n\n  return (\n    <Card className=\"border-border/50\">\n      <CardHeader className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 pb-2\">\n        <div className=\"space-y-1\">\n          <CardTitle className=\"text-base font-semibold\">Balance Over Time</CardTitle>\n          <CardDescription>Your total balance over time</CardDescription>\n        </div>\n      </CardHeader>\n      <CardContent>{chart}</CardContent>\n    </Card>\n  );\n}\n"
    },
    {
      "name": "src/components/dashboard/CashflowSankeyChart.tsx",
      "type": "text/plain",
      "content": "import { useMemo, useState, type PointerEvent } from \"react\";\nimport { sankey, sankeyLinkHorizontal, type SankeyNodeMinimal, type SankeyLinkMinimal } from \"d3-sankey\";\nimport { useParentSize } from \"@visx/responsive\";\nimport { localPoint } from \"@visx/event\";\nimport type { CashflowNode, CashflowLink, SankeyNodeKind } from \"@/lib/cashflowSankey\";\nimport { shareColor } from \"@/components/dashboard/shareColors\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { ChartTooltip } from \"@/components/charts/ChartTooltip\";\n\ntype LaidOutNode = CashflowNode & Required<Pick<SankeyNodeMinimal<CashflowNode, CashflowLink>, \"x0\" | \"x1\" | \"y0\" | \"y1\" | \"depth\">> & { value?: number };\ntype LaidOutLink = CashflowLink & SankeyLinkMinimal<CashflowNode, CashflowLink> & { source: LaidOutNode; target: LaidOutNode };\n\nconst NODE_WIDTH = 10;\nconst NODE_PADDING = 16;\nconst ROW_HEIGHT = 34;\n//: Node labels render outside the node rects (left column labels to their\n//: left, right column to their right), so the layout's own extent has to\n//: leave room for them or the SVG's default overflow:hidden clips every\n//: label off the canvas entirely.\nconst LABEL_MARGIN = 152;\n\n// Emphasis levels for hover/focus. A hovered node lights up every link\n// touching it (and those links' other endpoints); a hovered link lights up\n// only itself and its own two endpoints. Everything else fades.\nconst LINK_OPACITY = 0.4;\nconst LINK_OPACITY_CONNECTED = 0.8;\nconst LINK_OPACITY_ACTIVE = 0.9;\nconst LINK_OPACITY_DIMMED = 0.06;\nconst NODE_OPACITY_DIMMED = 0.25;\nconst TRANSITION = \"opacity 200ms ease, stroke-opacity 200ms ease\";\n\ntype Hover = { type: \"node\"; id: string } | { type: \"link\"; index: number } | null;\n\nfunction nodeFill(kind: SankeyNodeKind, expenseRank: number): string {\n  if (kind === \"income\" || kind === \"surplus\") return \"hsl(var(--chart-green))\";\n  if (kind === \"hub\") return \"hsl(var(--foreground))\";\n  return shareColor(expenseRank);\n}\n\n//: An income source is a merchant name and can be arbitrarily long; without\n//: a cap it would run past the reserved label margin regardless of size.\nfunction truncate(s: string, max = 18): string {\n  return s.length > max ? `${s.slice(0, max - 1)}…` : s;\n}\n\n/**\n * Income sources -> Cash flow -> expense categories (+ surplus), Sure's\n * dashboard Cashflow diagram. Built directly on d3-sankey (no visx\n * equivalent exists for this layout) with a plain SVG render: node rects\n * carry identity colour, each link is tinted by whichever endpoint isn't the\n * hub, so a flow visually belongs to the source or category it touches\n * rather than the neutral hub in the middle.\n */\nexport function CashflowSankeyChart({ nodes, links }: { nodes: CashflowNode[]; links: CashflowLink[] }) {\n  const { money, compact } = useMoney();\n  const { parentRef, width } = useParentSize({ debounceTime: 30 });\n  const [hover, setHover] = useState<Hover>(null);\n  const [pointer, setPointer] = useState<{ x: number; y: number } | null>(null);\n\n  const expenseRank = useMemo(() => {\n    const ranks = new Map<string, number>();\n    nodes.filter((n) => n.kind === \"expense\").forEach((n, i) => ranks.set(n.id, i));\n    return ranks;\n  }, [nodes]);\n\n  const maxColumnRows = useMemo(() => {\n    const byDepthCount = new Map<SankeyNodeKind, number>();\n    for (const n of nodes) byDepthCount.set(n.kind, (byDepthCount.get(n.kind) ?? 0) + 1);\n    return Math.max(byDepthCount.get(\"income\") ?? 0, (byDepthCount.get(\"expense\") ?? 0) + (byDepthCount.get(\"surplus\") ?? 0));\n  }, [nodes]);\n  const height = Math.max(220, maxColumnRows * ROW_HEIGHT + 40);\n\n  const { laidOutNodes, laidOutLinks } = useMemo(() => {\n    if (nodes.length === 0 || width === 0) return { laidOutNodes: [] as LaidOutNode[], laidOutLinks: [] as LaidOutLink[] };\n    const gen = sankey<CashflowNode, CashflowLink>()\n      .nodeId((d) => d.id)\n      .nodeWidth(NODE_WIDTH)\n      .nodePadding(NODE_PADDING)\n      .extent([\n        [LABEL_MARGIN, 10],\n        [Math.max(LABEL_MARGIN + 40, width - LABEL_MARGIN), height - 10],\n      ]);\n    const graph = gen({\n      nodes: nodes.map((d) => ({ ...d })),\n      links: links.map((d) => ({ ...d })),\n    });\n    return { laidOutNodes: graph.nodes as LaidOutNode[], laidOutLinks: graph.links as LaidOutLink[] };\n  }, [nodes, links, width, height]);\n\n  // Total flow through the hub — d3-sankey gives every node a `.value` of\n  // max(incoming, outgoing) when none is supplied explicitly, so the hub's is\n  // the total cash flow for the period. Link tooltips show each flow's share\n  // of this, not of their own column, so a link reads the same whether it's\n  // hovered on the income or the expense side.\n  const totalFlow = useMemo(\n    () => laidOutNodes.find((n) => n.kind === \"hub\")?.value ?? 0,\n    [laidOutNodes]\n  );\n\n  // What a hover lights up: the hovered thing itself, plus everything one\n  // hop away from it (a node's links and their other ends, or a link's own\n  // two endpoints).\n  const connected = useMemo(() => {\n    if (!hover) return null;\n    const nodeIds = new Set<string>();\n    const linkIndexes = new Set<number>();\n    if (hover.type === \"node\") {\n      nodeIds.add(hover.id);\n      laidOutLinks.forEach((l, i) => {\n        if (l.source.id === hover.id || l.target.id === hover.id) {\n          linkIndexes.add(i);\n          nodeIds.add(l.source.id);\n          nodeIds.add(l.target.id);\n        }\n      });\n    } else {\n      const l = laidOutLinks[hover.index];\n      if (l) {\n        linkIndexes.add(hover.index);\n        nodeIds.add(l.source.id);\n        nodeIds.add(l.target.id);\n      }\n    }\n    return { nodeIds, linkIndexes };\n  }, [hover, laidOutLinks]);\n\n  const linkOpacity = (index: number): number => {\n    if (!hover) return LINK_OPACITY;\n    if (hover.type === \"link\") return index === hover.index ? LINK_OPACITY_ACTIVE : LINK_OPACITY_DIMMED;\n    return connected?.linkIndexes.has(index) ? LINK_OPACITY_CONNECTED : LINK_OPACITY_DIMMED;\n  };\n\n  const nodeOpacity = (id: string): number => {\n    if (!hover) return 1;\n    return connected?.nodeIds.has(id) ? 1 : NODE_OPACITY_DIMMED;\n  };\n\n  const clearHover = () => { setHover(null); setPointer(null); };\n  const track = (e: PointerEvent<SVGElement>) => {\n    const pt = localPoint(e);\n    if (pt) setPointer({ x: pt.x, y: pt.y });\n  };\n\n  const linkPath = sankeyLinkHorizontal<CashflowNode, CashflowLink>();\n\n  if (nodes.length === 0) {\n    return (\n      <div className=\"flex items-center justify-center py-12 text-sm text-muted-foreground\">\n        No income or spending in this period.\n      </div>\n    );\n  }\n\n  const maxDepth = Math.max(...laidOutNodes.map((n) => n.depth), 0);\n  const hoveredLink = hover?.type === \"link\" ? laidOutLinks[hover.index] : undefined;\n\n  return (\n    <div ref={parentRef} className=\"relative w-full\" style={{ height }}>\n      {width > 0 && (\n        <svg\n          width={width}\n          height={height}\n          style={{ overflow: \"visible\" }}\n          role=\"img\"\n          aria-label=\"Cash flow from income sources through expense categories to surplus\"\n        >\n          <g>\n            {laidOutLinks.map((l, i) => {\n              // The link \"belongs\" to whichever side isn't the neutral hub,\n              // so income -> hub is coloured by the source and hub -> expense\n              // by the target.\n              const owner = l.source.kind === \"hub\" ? l.target : l.source;\n              const d = linkPath(l) ?? \"\";\n              const pct = totalFlow > 0 ? (l.value / totalFlow) * 100 : 0;\n              return (\n                <g key={i}>\n                  <path\n                    d={d}\n                    fill=\"none\"\n                    stroke={nodeFill(owner.kind, expenseRank.get(owner.id) ?? 0)}\n                    strokeOpacity={linkOpacity(i)}\n                    strokeWidth={Math.max(1, l.width ?? 0)}\n                    style={{ transition: TRANSITION }}\n                    pointerEvents=\"none\"\n                  />\n                  {/* Wider transparent hit area so thin flows and the gap\n                      right at a node's edge are still easy to hover, which\n                      is what caused the flicker moving from a node onto its\n                      links. */}\n                  <path\n                    d={d}\n                    fill=\"none\"\n                    stroke=\"transparent\"\n                    strokeWidth={Math.max(12, l.width ?? 0)}\n                    tabIndex={0}\n                    role=\"img\"\n                    aria-label={`${l.source.name} to ${l.target.name}: ${money(l.value)}, ${pct.toFixed(1)}%`}\n                    className=\"outline-none cursor-pointer\"\n                    onPointerEnter={(e) => { setHover({ type: \"link\", index: i }); track(e); }}\n                    onPointerMove={track}\n                    onPointerLeave={clearHover}\n                    onFocus={() => setHover({ type: \"link\", index: i })}\n                    onBlur={clearHover}\n                  />\n                </g>\n              );\n            })}\n          </g>\n          <g>\n            {laidOutNodes.map((n) => {\n              const fill = nodeFill(n.kind, expenseRank.get(n.id) ?? 0);\n              const isLeft = n.depth === 0;\n              const isRight = n.depth === maxDepth;\n              const cy = (n.y0 + n.y1) / 2;\n              const value = n.kind === \"hub\" ? undefined : n.value;\n              return (\n                <g\n                  key={n.id}\n                  tabIndex={n.kind === \"hub\" ? -1 : 0}\n                  role={n.kind === \"hub\" ? undefined : \"img\"}\n                  aria-label={n.kind === \"hub\" ? undefined : `${n.name}: ${value != null ? money(value) : \"\"}`}\n                  className=\"outline-none cursor-pointer\"\n                  style={{ opacity: nodeOpacity(n.id), transition: TRANSITION }}\n                  onPointerEnter={() => setHover({ type: \"node\", id: n.id })}\n                  onPointerLeave={clearHover}\n                  onFocus={() => setHover({ type: \"node\", id: n.id })}\n                  onBlur={clearHover}\n                >\n                  <rect\n                    x={n.x0}\n                    y={n.y0}\n                    width={Math.max(1, n.x1 - n.x0)}\n                    height={Math.max(1, n.y1 - n.y0)}\n                    rx={2}\n                    fill={fill}\n                  />\n                  {n.kind !== \"hub\" && (\n                    <text\n                      x={isLeft ? n.x0 - 8 : n.x1 + 8}\n                      y={cy}\n                      dy=\"0.33em\"\n                      textAnchor={isLeft ? \"end\" : isRight ? \"start\" : \"middle\"}\n                      fontSize={11}\n                      fill=\"hsl(var(--foreground))\"\n                      className=\"pointer-events-none\"\n                    >\n                      <title>{n.name}</title>\n                      {truncate(n.name)}\n                      {value != null && (\n                        <tspan fill=\"hsl(var(--muted-foreground))\" dx={4}>\n                          {compact(value)}\n                        </tspan>\n                      )}\n                    </text>\n                  )}\n                </g>\n              );\n            })}\n          </g>\n        </svg>\n      )}\n      {hoveredLink && pointer && (\n        <ChartTooltip x={pointer.x} y={pointer.y} containerWidth={width}>\n          <p className=\"font-medium text-foreground\">\n            {hoveredLink.source.name} → {hoveredLink.target.name}\n          </p>\n          <p className=\"text-muted-foreground\">\n            {money(hoveredLink.value)} ({totalFlow > 0 ? ((hoveredLink.value / totalFlow) * 100).toFixed(1) : \"0.0\"}%)\n          </p>\n        </ChartTooltip>\n      )}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/dashboard/CollapseToggle.tsx",
      "type": "text/plain",
      "content": "import { ChevronDown } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n/** Chevron button that shows or hides a dashboard section. */\nexport function CollapseToggle({ collapsed, onToggle, label, controls }: { collapsed: boolean; onToggle: () => void; label: string; controls: string }) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onToggle}\n      aria-expanded={!collapsed}\n      aria-controls={controls}\n      aria-label={`${collapsed ? \"Show\" : \"Hide\"} ${label}`}\n      className=\"inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground transition-colors\"\n    >\n      <ChevronDown size={16} strokeWidth={1.5} className={cn(\"transition-transform duration-200\", collapsed && \"-rotate-90\")} />\n    </button>\n  );\n}\n"
    },
    {
      "name": "src/components/dashboard/ForecastWidget.tsx",
      "type": "text/plain",
      "content": "import { useMemo } from \"react\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { cn } from \"@/lib/utils\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { TrendingUp, ArrowRight } from \"lucide-react\";\nimport { Link } from \"react-router-dom\";\nimport { monthKey } from \"@/lib/format\";\nimport { projectMonthEnd } from \"@/lib/budgets\";\n\n/**\n * Projected end-of-month balance.\n *\n * The previous version fetched budgets once on mount (so it never reflected an\n * edit), ignored income entirely, and used `max(totalBudget, actualSpend)` with\n * no sense of how far through the month it was, on the 2nd it projected a full\n * month of spending, on the 28th almost none. It now extrapolates real spend at\n * the current daily rate and counts income still expected.\n */\nexport function ForecastWidget() {\n  const { accounts, transactions } = useFinanceData({ summary: true });\n  const { money } = useMoney();\n\n  const currentBalance = useMemo(\n    () =>\n      accounts\n        .filter((a) => a.type === \"checking\" || a.type === \"savings\")\n        .reduce((s, a) => s + a.balance, 0),\n    [accounts]\n  );\n\n  const p = useMemo(\n    () => projectMonthEnd(transactions, currentBalance, monthKey(new Date())),\n    [transactions, currentBalance]\n  );\n\n  const isPositive = p.projectedBalance >= 0;\n  const hasData = currentBalance !== 0 || p.spentToDate > 0;\n  const spendRatio =\n    currentBalance > 0 ? Math.min((p.projectedSpend / currentBalance) * 100, 100) : 0;\n  const daysLeft = p.daysInMonth - p.daysElapsed;\n\n  if (!hasData) {\n    return (\n      <div className=\"flex flex-col items-center justify-center h-full gap-2 text-center\">\n        <TrendingUp size={18} className=\"text-muted-foreground/40\" />\n        <p className=\"text-[10px] text-muted-foreground\">Connect your bank to see a forecast</p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"h-full flex flex-col justify-between gap-3\">\n      <div className=\"space-y-1\">\n        <p className=\"text-[10px] uppercase tracking-wider text-muted-foreground font-medium\">\n          Projected end of month\n        </p>\n        <p\n          className={cn(\n            \"text-2xl font-bold tabular-nums tracking-tight\",\n            isPositive ? \"text-series-income\" : \"text-series-expense\"\n          )}\n        >\n          {money(p.projectedBalance)}\n        </p>\n        <p className=\"text-[10px] text-muted-foreground tabular-nums\">\n          {daysLeft > 0 ? `${daysLeft} days left · ` : \"\"}\n          {money(p.spentToDate)} spent so far\n        </p>\n      </div>\n\n      <div className=\"space-y-1.5\">\n        <Progress\n          value={spendRatio}\n          className={cn(\n            \"h-2 bg-muted/50 [&>div]:transition-all [&>div]:duration-700 [&>div]:ease-out\",\n            spendRatio > 85\n              ? \"[&>div]:bg-chart-red\"\n              : spendRatio > 60\n                ? \"[&>div]:bg-chart-yellow\"\n                : \"[&>div]:bg-chart-green\"\n          )}\n        />\n        <div className=\"flex justify-between text-[10px] text-muted-foreground tabular-nums\">\n          <span>Balance {money(currentBalance)}</span>\n          <span>Projected spend {money(p.projectedSpend)}</span>\n        </div>\n      </div>\n\n      <Link\n        to=\"/planning\"\n        className=\"flex items-center gap-1 text-[10px] text-primary hover:text-primary/80 font-medium self-start\"\n      >\n        View forecast <ArrowRight size={10} />\n      </Link>\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/dashboard/OutflowsWidget.tsx",
      "type": "text/plain",
      "content": "import { useMemo, useState } from \"react\";\nimport { useParentSize } from \"@visx/responsive\";\nimport { DonutChart } from \"@/components/charts/DonutChart\";\nimport { BubbleChart } from \"@/components/charts/BubbleChart\";\nimport { ChartTypeToggle } from \"@/components/charts/ChartTypeToggle\";\nimport { categorySpendBreakdown } from \"@/lib/categorySpend\";\nimport { shareColor } from \"@/components/dashboard/shareColors\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { PieChart, Circle } from \"lucide-react\";\nimport type { Transaction } from \"@/types/finance\";\n\n//: Slices beyond this rank fold into a single \"Other\" row, the same cutoff\n//: (and the same colour-cycling-to-muted fallback) the balance sheet's\n//: account list already uses via shareColor.\nconst MAX_SLICES = 5;\n\n/**\n * Dashboard \"Outflows\": this period's spending by category as a donut with\n * a ranked list beside it, matching Sure's dashboard widget of the same\n * name. Shares `categorySpendBreakdown` with the Analytics page's category\n * chart, so the two surfaces never disagree on what counts as spending.\n */\nconst CHART_TYPES = [\n  { value: \"donut\", icon: PieChart, label: \"Donut\" },\n  { value: \"bubbles\", icon: Circle, label: \"Bubbles\" },\n] as const;\n\nexport function OutflowsWidget({ transactions, donutOnly = false }: { transactions: Transaction[]; donutOnly?: boolean }) {\n  const { money, compact } = useMoney();\n  const { parentRef, width } = useParentSize({ debounceTime: 30 });\n  const [chartType, setChartType] = useState<\"donut\" | \"bubbles\">(\"donut\");\n  const breakdown = useMemo(() => categorySpendBreakdown(transactions), [transactions]);\n  const total = breakdown.reduce((s, d) => s + d.value, 0);\n\n  const top = breakdown.slice(0, MAX_SLICES);\n  const rest = breakdown.slice(MAX_SLICES);\n  const otherValue = rest.reduce((s, d) => s + d.value, 0);\n  const rows = otherValue > 0\n    ? [...top, { name: \"Other\", value: otherValue, pct: total > 0 ? (otherValue / total) * 100 : 0 }]\n    : top;\n\n  if (breakdown.length === 0) {\n    return (\n      <div className=\"flex items-center justify-center py-8 text-sm text-muted-foreground\">\n        No spending in this period.\n      </div>\n    );\n  }\n\n  const slices = rows.map((r, i) => ({ name: r.name, value: r.value, color: shareColor(i) }));\n\n  if (donutOnly) {\n    return (\n      <div ref={parentRef} className=\"mx-auto w-full max-w-[168px]\">\n        {width > 0 && <DonutChart data={slices} size={Math.min(168, width)} thickness={20} formatValue={compact} centerLabel=\"This month\" centerValue={money(total)} />}\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"space-y-3\">\n      <div className=\"flex justify-end\">\n        <ChartTypeToggle options={CHART_TYPES} value={chartType} onChange={setChartType} label=\"Chart type\" />\n      </div>\n      <div className=\"flex flex-col sm:flex-row items-center gap-6\">\n        {chartType === \"donut\" ? (\n          <DonutChart\n            data={slices}\n            size={168}\n            thickness={20}\n            formatValue={compact}\n            centerLabel=\"Spent\"\n            centerValue={money(total)}\n          />\n        ) : (\n          <BubbleChart data={slices} size={168} formatValue={compact} />\n        )}\n        <div className=\"flex-1 w-full space-y-2.5\">\n          {rows.map((r, i) => (\n            <div key={r.name} className=\"space-y-1\">\n              <div className=\"flex items-center justify-between gap-2 text-sm\">\n                <span className=\"flex items-center gap-2 min-w-0\">\n                  <span aria-hidden className=\"h-2.5 w-2.5 rounded-full shrink-0\" style={{ backgroundColor: shareColor(i) }} />\n                  <span className=\"text-foreground truncate\">{r.name}</span>\n                </span>\n                <span className=\"text-muted-foreground tabular-nums shrink-0\">\n                  <span className=\"text-foreground font-medium\">{money(r.value)}</span> · {r.pct.toFixed(0)}%\n                </span>\n              </div>\n              <div className=\"h-1 rounded-full bg-muted overflow-hidden\" aria-hidden>\n                <div className=\"h-full rounded-full\" style={{ width: `${Math.max(2, r.pct)}%`, backgroundColor: shareColor(i) }} />\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/dashboard/RecentTransactions.tsx",
      "type": "text/plain",
      "content": "import { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { cn } from \"@/lib/utils\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { refinedMerchantLabel } from \"@/lib/merchant\";\nimport { normalizeCategory, UNCATEGORIZED } from \"@/lib/categories\";\nimport { Link, useNavigate } from \"react-router-dom\";\nimport { MerchantAvatar } from \"@/components/elements/CategoryIcon\";\nimport { CollapseToggle } from \"@/components/dashboard/CollapseToggle\";\nimport { useCollapsed } from \"@/hooks/useCollapsed\";\nimport { useMerchantRefinements } from \"@/hooks/useMerchantRefinements\";\n\nexport function RecentTransactions({ rail = false, limit }: { rail?: boolean; limit?: number }) {\n  const { transactions } = useFinanceData();\n  const { moneyAbs, date: fmtDate } = useMoney();\n  const navigate = useNavigate();\n  const [collapsed, toggle] = useCollapsed(\"recent\");\n  const { data: refinements } = useMerchantRefinements();\n  // A transaction with no date (a malformed import/sync row) has no\n  // meaningful recency and would crash the sort below (comparing null).\n  const recent = transactions\n    .filter((t) => t.date)\n    .sort((a, b) => b.date.localeCompare(a.date))\n    .slice(0, limit ?? (rail ? 14 : 8));\n\n  if (recent.length === 0) {\n    return (\n      <Card className=\"border-border/50\">\n        <CardContent className=\"p-8 text-center text-muted-foreground text-sm\">\n          No recent transactions.\n        </CardContent>\n      </Card>\n    );\n  }\n\n  return (\n    <Card className=\"border-border/50\">\n      <CardHeader className={cn(\"pb-3 flex flex-row items-center justify-between\", rail && \"px-4\")}>\n        <div className=\"flex items-center gap-1\">\n          <CardTitle className=\"text-sm font-medium text-muted-foreground\">Recent transactions</CardTitle>\n          <CollapseToggle collapsed={collapsed} onToggle={toggle} label=\"recent transactions\" controls=\"dash-recent\" />\n        </div>\n        <Link to=\"/transactions\" className=\"text-xs text-primary hover:text-primary/80 font-medium\">View all</Link>\n      </CardHeader>\n      <CardContent id=\"dash-recent\" className={cn(\"pt-0\", rail && \"px-4\", collapsed && \"hidden\")}>\n        <div className=\"space-y-0\">\n          {recent.map((txn) => (\n            <div\n              key={txn.id}\n              className={cn(\n                \"border-b border-border/30 last:border-0 hover:bg-muted/30 transition-all duration-150 cursor-pointer rounded-md px-2 -mx-2\",\n                rail ? \"py-2.5\" : \"flex items-center justify-between py-3\"\n              )}\n              onClick={() => navigate(`/transactions/${txn.id}`)}\n            >\n              {rail ? (\n                <>\n                  <div className=\"flex items-baseline justify-between gap-2\">\n                    <p className=\"text-[13px] font-medium text-foreground truncate\" title={txn.description}>\n                      {refinedMerchantLabel(txn.description, refinements, txn.date, txn.category)}\n                    </p>\n                    <span className={cn(\"text-[13px] font-medium tabular-nums shrink-0\", txn.amount >= 0 ? \"text-chart-green\" : \"text-foreground\")}>\n                      {txn.amount >= 0 ? \"+\" : \"-\"}{moneyAbs(txn.amount)}\n                    </span>\n                  </div>\n                  <p className=\"text-[11px] text-muted-foreground truncate\">\n                    {fmtDate(txn.date, \"short\")}\n                    {normalizeCategory(txn.category) !== UNCATEGORIZED && <span> · {normalizeCategory(txn.category)}</span>}\n                  </p>\n                </>\n              ) : (\n                <>\n                  <div className=\"flex items-center gap-3 min-w-0\">\n                    <MerchantAvatar description={txn.description} category={txn.category} />\n                    <div className=\"min-w-0\">\n                      <p className=\"text-sm font-medium text-foreground truncate\" title={txn.description}>\n                        {refinedMerchantLabel(txn.description, refinements, txn.date, txn.category)}\n                      </p>\n                      <p className=\"text-xs text-muted-foreground\">\n                        {fmtDate(txn.date)}\n                        {normalizeCategory(txn.category) !== UNCATEGORIZED && (\n                          <span> · {normalizeCategory(txn.category)}</span>\n                        )}\n                      </p>\n                    </div>\n                  </div>\n                  <span className={cn(\"text-sm font-medium tabular-nums ml-3 shrink-0\", txn.amount >= 0 ? \"text-chart-green\" : \"text-foreground\")}>\n                    {txn.amount >= 0 ? \"+\" : \"-\"}{moneyAbs(txn.amount)}\n                  </span>\n                </>\n              )}\n            </div>\n          ))}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n"
    },
    {
      "name": "src/components/dashboard/StatCard.tsx",
      "type": "text/plain",
      "content": "import { Card, CardContent, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\nimport { TrendingUp, TrendingDown } from \"lucide-react\";\nimport { Link } from \"react-router-dom\";\n\ninterface StatCardProps {\n  title: string;\n  value: string;\n  change?: string;\n  changeType?: \"positive\" | \"negative\" | \"neutral\";\n  description?: string;\n  href?: string;\n}\n\nexport function StatCard({ title, value, change, changeType = \"neutral\", description, href }: StatCardProps) {\n  const content = (\n    <Card className={cn(\"bg-card border-border transition-all duration-200\", href && \"hover:shadow-md hover:-translate-y-0.5 cursor-pointer\")}>\n      <CardHeader className=\"pb-2\">\n        <div className=\"flex items-center justify-between\">\n          <CardTitle className=\"text-sm font-medium text-muted-foreground tracking-normal\">{title}</CardTitle>\n          {change && (\n            <Badge\n              variant=\"secondary\"\n              className={cn(\n                \"text-[10px] font-medium tabular-nums px-1.5 py-0 h-5 border\",\n                changeType === \"positive\" && \"bg-chart-green/15 text-chart-green border-chart-green/30\",\n                changeType === \"negative\" && \"bg-chart-red/15 text-chart-red border-chart-red/30\",\n                changeType === \"neutral\" && \"bg-muted text-muted-foreground border-border\"\n              )}\n            >\n              {changeType === \"positive\" && <TrendingUp size={10} strokeWidth={1.5} className=\"mr-0.5\" />}\n              {changeType === \"negative\" && <TrendingDown size={10} strokeWidth={1.5} className=\"mr-0.5\" />}\n              {change}\n            </Badge>\n          )}\n        </div>\n      </CardHeader>\n      <CardContent className=\"pb-0\">\n        <p className=\"text-2xl font-bold text-foreground tracking-tight tabular-nums\">{value}</p>\n      </CardContent>\n      {description && (\n        <CardFooter className=\"pt-2 pb-4\">\n          <p className=\"text-xs text-muted-foreground\">{description}</p>\n        </CardFooter>\n      )}\n    </Card>\n  );\n\n  if (href) return <Link to={href}>{content}</Link>;\n  return content;\n}\n"
    },
    {
      "name": "src/components/dashboard/UpcomingList.tsx",
      "type": "text/plain",
      "content": "import { differenceInCalendarDays } from \"date-fns\";\nimport { X } from \"lucide-react\";\nimport { CategoryIcon } from \"@/components/elements/CategoryIcon\";\nimport { Button } from \"@/components/ui/button\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { parseLocalDate, cn } from \"@/lib/utils\";\nimport { useUpcomingRecurring, useRecurringMutations } from \"@/hooks/useRecurring\";\nimport { CADENCE_LABEL } from \"@/lib/recurring\";\nimport type { RecurringDto } from \"@/lib/api\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nfunction dueLabel(dateStr: string, today = new Date()): string {\n  const days = differenceInCalendarDays(parseLocalDate(dateStr), today);\n  if (days <= 0) return \"Due today\";\n  if (days === 1) return \"Tomorrow\";\n  return `In ${days} days`;\n}\n\n/**\n * Predicted next occurrences of the user's detected bills, subscriptions and\n * paychecks, soonest first. Shared by the dashboard's Upcoming widget and\n * the Transactions page's Upcoming section, one query and one dismiss\n * action so a merchant dismissed in either place disappears from both.\n */\nexport function UpcomingList({ days = 30, limit }: { days?: number; limit?: number }) {\n  const { data, isLoading } = useUpcomingRecurring(days);\n  const { dismiss } = useRecurringMutations(days);\n  const { money } = useMoney();\n\n  const rows = limit ? (data ?? []).slice(0, limit) : data ?? [];\n\n  // This query is in flight from the moment the page mounts, same as\n  // everything else on it, but returning null while it resolves left this\n  // one section visibly blank until it did - a pop-in the rest of the page\n  // (which shows a skeleton instead) doesn't have. A plain `null` here\n  // was never noticeable on the dashboard, where it sits well below the\n  // fold, but the Transactions page puts it right in view.\n  if (isLoading) {\n    return (\n      <div className=\"space-y-1\">\n        {Array.from({ length: limit ?? 3 }).map((_, i) => (\n          <div key={i} className=\"flex items-center gap-3 py-2 px-2 -mx-2\">\n            <Skeleton className=\"h-8 w-8 rounded-full shrink-0\" />\n            <div className=\"min-w-0 flex-1 space-y-1.5\">\n              <Skeleton className=\"h-3.5 w-32\" />\n              <Skeleton className=\"h-3 w-20\" />\n            </div>\n            <Skeleton className=\"h-3.5 w-14 shrink-0\" />\n          </div>\n        ))}\n      </div>\n    );\n  }\n\n  if (rows.length === 0) {\n    return (\n      <p className=\"text-sm text-muted-foreground text-center py-6\">\n        Nothing recurring due in the next {days} days.\n      </p>\n    );\n  }\n\n  return (\n    <div className=\"space-y-1\">\n      {rows.map((r: RecurringDto) => (\n        <div\n          key={r.id}\n          className=\"group/row flex items-center gap-3 py-2 px-2 -mx-2 rounded-lg hover:bg-muted/50 transition-colors\"\n        >\n          <CategoryIcon category={r.category || \"Uncategorized\"} size=\"sm\" />\n          <div className=\"min-w-0 flex-1\" title={`${r.merchantName} · ${CADENCE_LABEL[r.cadence]} · ${dueLabel(r.nextDate)}`}>\n            <p className=\"text-sm font-medium text-foreground truncate\">{r.merchantName}</p>\n            <p className=\"text-xs text-muted-foreground truncate\">\n              {CADENCE_LABEL[r.cadence]} · {dueLabel(r.nextDate)}\n            </p>\n          </div>\n          <span className={cn(\"text-sm font-medium tabular-nums shrink-0 text-right\", r.type === \"income\" ? \"text-chart-green\" : \"text-foreground\")}>\n            {r.type === \"income\" ? \"+\" : \"-\"}{money(r.typicalAmount)}\n          </span>\n          <Button\n            variant=\"ghost\" size=\"icon\"\n            className=\"h-6 w-6 shrink-0 opacity-0 group-hover/row:opacity-100 text-muted-foreground hover:text-destructive transition-opacity\"\n            aria-label={`Not recurring: ${r.merchantName}`}\n            disabled={dismiss.isPending}\n            onClick={() => dismiss.mutate(r.id)}\n          >\n            <X size={12} strokeWidth={2} />\n          </Button>\n        </div>\n      ))}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/dashboard/WeightBar.tsx",
      "type": "text/plain",
      "content": "import { cn } from \"@/lib/utils\";\nimport { SHARE_COLORS, shareColor } from \"./shareColors\";\n\n/**\n * One thin bar split into each item's share, with a 2px surface gap between\n * segments. Identity comes from the matching dot on each row below, so the\n * bar never relies on colour alone.\n */\nexport function WeightBar({ shares, label, className }: { shares: { key: string; share: number }[]; label: string; className?: string }) {\n  const shown = shares.filter((s) => s.share > 0);\n  const main = shown.slice(0, SHARE_COLORS.length);\n  const other = shown.slice(SHARE_COLORS.length).reduce((s, x) => s + x.share, 0);\n  const segments = [...main.map((s, i) => ({ ...s, color: shareColor(i) })), ...(other > 0 ? [{ key: \"other\", share: other, color: shareColor(99) }] : [])];\n  return (\n    <div role=\"img\" aria-label={label} className={cn(\"flex h-1.5 w-full gap-[2px] overflow-hidden rounded-full\", className)}>\n      {segments.length === 0 ? (\n        <div className=\"h-full w-full bg-muted\" />\n      ) : (\n        segments.map((s) => (\n          <div key={s.key} className=\"h-full first:rounded-l-full last:rounded-r-full\" style={{ width: `${s.share}%`, backgroundColor: s.color }} />\n        ))\n      )}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/dashboard/shareColors.ts",
      "type": "text/plain",
      "content": "/** Categorical order for account shares; past these, accounts fold into \"Other\". */\nexport const SHARE_COLORS = [\"--chart-blue\", \"--chart-purple\", \"--chart-green\", \"--chart-orange\", \"--chart-yellow\"] as const;\nconst OTHER = \"--muted-foreground\";\n\n/** The colour for the i-th item of a side, so the bar and the row dots agree. */\nexport function shareColor(i: number) {\n  return `hsl(var(${SHARE_COLORS[i] ?? OTHER}))`;\n}\n"
    },
    {
      "name": "src/components/elements/AnimatedMoney.tsx",
      "type": "text/plain",
      "content": "import NumberFlow from \"@number-flow/react\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { formatMoney, localeFor, maskedMoney } from \"@/lib/format\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A headline amount that counts to its new value when it changes (a refresh,\n * switching month). Uses the user's currency setting by default; in privacy\n * mode it shows the mask instead. NumberFlow honours prefers-reduced-motion\n * on its own.\n *\n * Pass `currency` to override the app-wide setting for amounts that carry\n * their own currency regardless of it, e.g. an investment holding priced in\n * its native market's currency.\n */\nexport function AnimatedMoney({\n  value,\n  currency: currencyOverride,\n  signDisplay = \"auto\",\n  className,\n}: {\n  value: number;\n  currency?: string;\n  signDisplay?: \"auto\" | \"exceptZero\" | \"never\";\n  className?: string;\n}) {\n  const { currency: appCurrency, hidden } = useMoney();\n  const currency = currencyOverride ?? appCurrency;\n  if (hidden) {\n    return (\n      <span className={cn(\"tabular-nums\", className)}>\n        {maskedMoney(currency, Number.isFinite(value) ? value : 0, { signDisplay })}\n      </span>\n    );\n  }\n  const n = Number.isFinite(value) ? value : 0;\n  const format = { style: \"currency\", currency, minimumFractionDigits: 2, maximumFractionDigits: 2, signDisplay } as const;\n  let label: string;\n  try {\n    label = new Intl.NumberFormat(localeFor(currency), format).format(n);\n  } catch {\n    // A currency setting Intl doesn't recognise: show the plain formatter's\n    // fallback text rather than letting NumberFlow throw mid-render.\n    return <span className={cn(\"tabular-nums\", className)}>{formatMoney(n, currency, { signDisplay })}</span>;\n  }\n  return (\n    <span className={className}>\n      {/* NumberFlow renders spinning digit strips in its shadow DOM, which read\n          as noise to assistive tech; they get a plain-text twin instead. */}\n      <NumberFlow value={n} locales={localeFor(currency)} format={format} className=\"tabular-nums\" aria-hidden />\n      <span className=\"sr-only\">{label}</span>\n    </span>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/BulkTagPicker.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { Check, ChevronDown, Plus, Tag as TagIcon } from \"lucide-react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from \"@/components/ui/command\";\nimport { Button } from \"@/components/ui/button\";\nimport type { TagDto } from \"@/lib/api\";\nimport { tagColorValue } from \"@/lib/tagColors\";\nimport { cn } from \"@/lib/utils\";\n\nfunction TagDot({ tag }: { tag: TagDto }) {\n  return <span aria-hidden className=\"h-2 w-2 rounded-full shrink-0\" style={{ backgroundColor: tagColorValue(tag.color) }} />;\n}\n\ninterface Props {\n  tags: TagDto[];\n  /** How many records this would apply to, for the button's confirm label. */\n  count: number;\n  onApply: (tagIds: string[]) => void;\n  /** Creates a new tag (with the default colour) and returns its id, so it\n   * can be picked immediately without a second round trip. */\n  onCreate?: (name: string) => Promise<string>;\n  disabled?: boolean;\n}\n\n/**\n * Multi-select tag picker for the bulk-selection floating bar: unlike\n * TagPicker (one transaction, shows and toggles its current tags), this has\n * no \"current\" state to show - the selection is a mix of transactions with\n * different tags already on them - so it only ever adds. Picks build up\n * locally and apply together in one call when \"Apply\" is pressed, rather\n * than firing a request per tag.\n */\nexport function BulkTagPicker({ tags, count, onApply, onCreate, disabled }: Props) {\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const [picked, setPicked] = useState<string[]>([]);\n  const [creating, setCreating] = useState(false);\n\n  const q = query.trim().toLowerCase();\n  const shown = q ? tags.filter((t) => t.name.toLowerCase().includes(q)) : tags;\n  const exactMatch = tags.some((t) => t.name.toLowerCase() === q);\n\n  const toggle = (id: string) => {\n    setPicked((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));\n  };\n\n  const reset = () => {\n    setPicked([]);\n    setQuery(\"\");\n  };\n\n  const createAndPick = async () => {\n    if (!onCreate || !query.trim() || creating) return;\n    setCreating(true);\n    try {\n      const id = await onCreate(query.trim());\n      setPicked((prev) => [...prev, id]);\n      setQuery(\"\");\n    } finally {\n      setCreating(false);\n    }\n  };\n\n  const apply = () => {\n    if (picked.length === 0) return;\n    onApply(picked);\n    setOpen(false);\n    reset();\n  };\n\n  return (\n    <Popover open={open} onOpenChange={(o) => { setOpen(o); if (!o) reset(); }} modal>\n      <PopoverTrigger asChild disabled={disabled}>\n        <Button variant=\"outline\" size=\"sm\" className=\"h-8 text-xs gap-1.5 bg-background font-normal\">\n          <TagIcon size={12} strokeWidth={1.5} />\n          Tag\n          <ChevronDown className=\"h-3.5 w-3.5 opacity-50 ml-0.5\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align=\"center\" className=\"w-[220px] p-0\">\n        <Command shouldFilter={false}>\n          <CommandInput placeholder=\"Search or create…\" className=\"h-9\" value={query} onValueChange={setQuery} />\n          <CommandList className=\"max-h-[260px]\">\n            {shown.length === 0 && (\n              <CommandEmpty>\n                {onCreate && query.trim() ? (\n                  <button\n                    type=\"button\"\n                    onClick={() => void createAndPick()}\n                    disabled={creating}\n                    className={cn(\"flex w-full items-center gap-2 px-2 py-1.5 text-sm text-left rounded-sm hover:bg-accent\", creating && \"opacity-50\")}\n                  >\n                    <Plus size={14} strokeWidth={1.75} />\n                    Create \"{query.trim()}\"\n                  </button>\n                ) : (\n                  \"No tags found.\"\n                )}\n              </CommandEmpty>\n            )}\n            <CommandGroup>\n              {shown.map((t) => (\n                <CommandItem key={t.id} value={t.name} onSelect={() => toggle(t.id)} className=\"gap-2\">\n                  <TagDot tag={t} />\n                  <span className=\"flex-1 truncate\">{t.name}</span>\n                  {picked.includes(t.id) && <Check className=\"h-4 w-4 text-primary\" />}\n                </CommandItem>\n              ))}\n              {onCreate && query.trim() && !exactMatch && shown.length > 0 && (\n                <CommandItem value={`__create__${query}`} onSelect={() => void createAndPick()} className=\"gap-2 text-primary\">\n                  <Plus size={14} strokeWidth={1.75} />\n                  Create \"{query.trim()}\"\n                </CommandItem>\n              )}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n        {picked.length > 0 && (\n          <div className=\"border-t border-border p-2\">\n            <Button size=\"sm\" className=\"w-full h-8 text-xs\" onClick={apply}>\n              Apply {picked.length} tag{picked.length === 1 ? \"\" : \"s\"} to {count} transaction{count === 1 ? \"\" : \"s\"}\n            </Button>\n          </div>\n        )}\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/CategoryIcon.tsx",
      "type": "text/plain",
      "content": "import { categoryBadgeClass, normalizeCategory } from \"@/lib/categories\";\nimport { normalizeMerchant } from \"@/lib/merchant\";\nimport { cn } from \"@/lib/utils\";\nimport { CATEGORY_ICON } from \"./categoryIcons\";\n\nconst SIZES = {\n  xs: { box: \"h-5 w-5\", icon: 11, text: \"text-[10px]\" },\n  sm: { box: \"h-6 w-6\", icon: 12, text: \"text-[11px]\" },\n  md: { box: \"h-8 w-8\", icon: 14, text: \"text-xs\" },\n} as const;\n\ntype Size = keyof typeof SIZES;\n\n/** A category's icon on a disc tinted with the category's badge colour. */\nexport function CategoryIcon({ category, size = \"md\", className }: { category: string; size?: Size; className?: string }) {\n  const c = normalizeCategory(category);\n  const Icon = CATEGORY_ICON[c];\n  const s = SIZES[size];\n  return (\n    <span aria-hidden className={cn(\"inline-flex items-center justify-center rounded-full shrink-0\", s.box, categoryBadgeClass(c), className)}>\n      <Icon size={s.icon} strokeWidth={1.75} />\n    </span>\n  );\n}\n\n/**\n * Round avatar for a transaction: the merchant's initial, tinted with its\n * category's colour. The initial comes from the cleaned merchant name, the\n * raw description starts with bank noise, so \"CHECKCARD 0411 STARBUCKS\" used to\n * render as \"C\".\n */\nexport function MerchantAvatar({\n  description,\n  category,\n  size = \"md\",\n  className,\n}: {\n  description: string;\n  category: string;\n  size?: Size;\n  className?: string;\n}) {\n  const name = normalizeMerchant(description) || description;\n  const initial = (name.match(/[\\p{L}\\p{N}]/u)?.[0] ?? \"?\").toUpperCase();\n  const s = SIZES[size];\n  return (\n    <span\n      aria-hidden\n      className={cn(\n        \"inline-flex items-center justify-center rounded-full shrink-0 font-semibold\",\n        s.box,\n        s.text,\n        categoryBadgeClass(category),\n        className\n      )}\n    >\n      {initial}\n    </span>\n  );\n}\n\n/** The category chip: icon + name, in the category's badge colours. */\nexport function CategoryBadge({ category, className }: { category: string; className?: string }) {\n  const c = normalizeCategory(category);\n  const Icon = CATEGORY_ICON[c];\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full border max-w-full\",\n        categoryBadgeClass(c),\n        className\n      )}\n    >\n      <Icon size={11} strokeWidth={1.75} className=\"shrink-0\" aria-hidden />\n      <span className=\"truncate\">{category}</span>\n    </span>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/CategoryPicker.tsx",
      "type": "text/plain",
      "content": "import { useContext } from \"react\";\nimport { CategoryCatalogContext } from \"@/context/CategoryCatalogContext\";\nimport { useState, type ReactNode } from \"react\";\nimport { Check, ChevronDown, LayoutGrid } from \"lucide-react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from \"@/components/ui/command\";\nimport { CATEGORIES } from \"@/lib/categories\";\nimport { cn } from \"@/lib/utils\";\nimport { CategoryIcon } from \"./CategoryIcon\";\n\nexport const ALL_CATEGORIES = \"all\";\n\ninterface Props {\n  value: string;\n  onChange: (category: string) => void;\n  categories?: readonly string[];\n  /** Adds an \"all\" row (value `\"all\"`) above the categories, for filters. */\n  allLabel?: string;\n  placeholder?: string;\n  /** Replaces the default select-style trigger, e.g. a bulk-action button. */\n  trigger?: ReactNode;\n  triggerClassName?: string;\n  align?: \"start\" | \"center\" | \"end\";\n  disabled?: boolean;\n  \"aria-label\"?: string;\n}\n\n/**\n * Searchable category picker. Twenty categories is past what a plain select\n * list scans comfortably, so typing narrows the list; each row carries the\n * category's icon and colour so the choice is recognisable before it is read.\n *\n * The default trigger matches the app's select trigger, so it drops into any\n * form or filter row in place of a <Select>.\n */\nexport function CategoryPicker({\n  value,\n  onChange,\n  categories: suppliedCategories,\n  allLabel,\n  placeholder = \"Category\",\n  trigger,\n  triggerClassName,\n  align = \"start\",\n  disabled,\n  \"aria-label\": ariaLabel = \"Category\",\n}: Props) {\n  const custom = useContext(CategoryCatalogContext);\n  const categories = suppliedCategories ?? [...CATEGORIES, ...custom];\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const pick = (c: string) => {\n    onChange(c);\n    setOpen(false);\n  };\n  // Plain substring match in the list's own order. cmdk's fuzzy scorer\n  // re-ranks rows while typing, which floated \"All\" into the middle and put\n  // \"Pets\" above \"Shopping\" for an \"s\".\n  const q = query.trim().toLowerCase();\n  const shown = q ? categories.filter((c) => c.toLowerCase().includes(q)) : categories;\n  const isAll = !!allLabel && value === ALL_CATEGORIES;\n  const hasValue = isAll || categories.includes(value);\n\n  return (\n    // Modal, so the list scrolls when the picker sits inside a Dialog or Sheet\n    // (their scroll lock otherwise swallows wheel events on the portal).\n    <Popover\n      open={open}\n      onOpenChange={(o) => {\n        setOpen(o);\n        if (!o) setQuery(\"\");\n      }}\n      modal\n    >\n      <PopoverTrigger asChild disabled={disabled}>\n        {trigger ?? (\n          <button\n            type=\"button\"\n            role=\"combobox\"\n            aria-expanded={open}\n            aria-label={ariaLabel}\n            className={cn(\n              \"flex h-10 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n              triggerClassName\n            )}\n          >\n            <span className=\"flex items-center gap-2 min-w-0\">\n              {hasValue && !isAll && <CategoryIcon category={value} size=\"xs\" />}\n              <span className={cn(\"truncate\", !hasValue && \"text-muted-foreground\")}>\n                {isAll ? allLabel : hasValue ? value : placeholder}\n              </span>\n            </span>\n            <ChevronDown className=\"h-4 w-4 opacity-50 shrink-0\" />\n          </button>\n        )}\n      </PopoverTrigger>\n      <PopoverContent align={align} className=\"w-[240px] p-0\">\n        <Command shouldFilter={false}>\n          <CommandInput placeholder=\"Search categories…\" className=\"h-9\" value={query} onValueChange={setQuery} />\n          <CommandList className=\"max-h-[300px]\">\n            <CommandEmpty>No category found.</CommandEmpty>\n            <CommandGroup>\n              {allLabel && !q && (\n                <CommandItem value={allLabel} onSelect={() => pick(ALL_CATEGORIES)} className=\"gap-2\">\n                  <span className=\"inline-flex h-5 w-5 items-center justify-center rounded-full bg-muted text-muted-foreground\">\n                    <LayoutGrid size={11} strokeWidth={1.75} />\n                  </span>\n                  <span className=\"flex-1\">{allLabel}</span>\n                  {isAll && <Check className=\"h-4 w-4 text-primary\" />}\n                </CommandItem>\n              )}\n              {shown.map((c) => (\n                <CommandItem key={c} value={c} onSelect={() => pick(c)} className=\"gap-2\">\n                  <CategoryIcon category={c} size=\"xs\" />\n                  <span className=\"flex-1 truncate\">{c}</span>\n                  {value === c && <Check className=\"h-4 w-4 text-primary\" />}\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/CopyButton.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Copy, Check } from \"lucide-react\";\nimport { toast } from \"sonner\";\n\nasync function copyToClipboard(text: string): Promise<boolean> {\n  try {\n    await navigator.clipboard.writeText(text);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n/** An icon button that copies `value`, with its own brief checkmark\n * confirmation - no toast on success, just the icon swap, so copying\n * several fields in a row doesn't stack toasts. */\nexport function CopyButton({ value, label, className }: { value: string; label: string; className?: string }) {\n  const [copied, setCopied] = useState(false);\n  return (\n    <Button\n      variant=\"outline\"\n      size=\"icon\"\n      className={className ?? \"h-8 w-8 shrink-0\"}\n      aria-label={label}\n      onClick={async () => {\n        const ok = await copyToClipboard(value);\n        if (ok) { setCopied(true); setTimeout(() => setCopied(false), 1500); }\n        else toast.error(\"Couldn't copy\");\n      }}\n    >\n      {copied ? <Check size={13} className=\"text-chart-green\" /> : <Copy size={13} strokeWidth={1.5} />}\n    </Button>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/DatePicker.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { Calendar as CalendarIcon, X } from \"lucide-react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport { Button } from \"@/components/ui/button\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { toDateString } from \"@/lib/format\";\nimport { parseLocalDate, cn } from \"@/lib/utils\";\n\ninterface Props {\n  /** \"YYYY-MM-DD\", or null/\"\" for no date picked. */\n  value: string | null;\n  onChange: (value: string | null) => void;\n  /** No date after this one is selectable. */\n  maxDate?: Date;\n  /** No date before this one is selectable. */\n  minDate?: Date;\n  placeholder?: string;\n  /** Shows a small clear button once a date is picked - for optional dates\n   * (a todo's due date, an asset's purchase date). Omit for a date that's\n   * always required (a report range's start/end). */\n  clearable?: boolean;\n  className?: string;\n  \"aria-label\"?: string;\n}\n\n/**\n * The app's own calendar popover in place of the browser's native\n * `<input type=\"date\">`, whose chrome (icon, dropdown styling, light/dark\n * behavior) is entirely OS/browser-drawn and can't be made to match the\n * rest of the UI. Every other date-pickable field already used this\n * Popover+Calendar pattern inline (AddTransactionDialog, Reports' own\n * previous picker, GoalsTracker, the investment dialogs); this pulls it\n * into one place instead of a sixth copy.\n */\nexport function DatePicker({\n  value, onChange, maxDate, minDate, placeholder = \"Pick a date\", clearable, className, \"aria-label\": ariaLabel,\n}: Props) {\n  const [open, setOpen] = useState(false);\n  const { date: fmt } = useMoney();\n  const selected = value ? parseLocalDate(value) : undefined;\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          aria-label={ariaLabel ?? placeholder}\n          className={cn(\"w-full justify-start text-left font-normal gap-2\", !value && \"text-muted-foreground\", className)}\n        >\n          <CalendarIcon className=\"h-4 w-4 shrink-0\" />\n          <span className=\"truncate flex-1\">{value ? fmt(value, \"medium\") : placeholder}</span>\n          {clearable && value && (\n            <span\n              role=\"button\"\n              tabIndex={0}\n              aria-label=\"Clear date\"\n              onClick={(e) => { e.stopPropagation(); onChange(null); }}\n              onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.stopPropagation(); onChange(null); } }}\n              className=\"rounded-sm p-0.5 -mr-1 hover:bg-muted-foreground/20 transition-colors shrink-0\"\n            >\n              <X size={12} strokeWidth={2} />\n            </span>\n          )}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto p-0 z-[100]\" align=\"start\">\n        <Calendar\n          mode=\"single\"\n          selected={selected}\n          onSelect={(d) => { if (d) onChange(toDateString(d)); setOpen(false); }}\n          disabled={(d) => (maxDate != null && d > maxDate) || (minDate != null && d < minDate)}\n          defaultMonth={selected ?? maxDate}\n          initialFocus\n        />\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/DetailRow.tsx",
      "type": "text/plain",
      "content": "import type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/** A label on the left, its value on the right, the rows of a detail panel. */\nexport function DetailRow({ label, children, className }: { label: string; children: ReactNode; className?: string }) {\n  return (\n    <div className={cn(\"flex items-center justify-between gap-4 py-1.5 text-sm\", className)}>\n      <dt className=\"text-muted-foreground shrink-0\">{label}</dt>\n      <dd className=\"text-foreground font-medium text-right min-w-0 truncate\">{children}</dd>\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/EmptyState.tsx",
      "type": "text/plain",
      "content": "import type { ReactNode } from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * One empty state for the whole app. `page` is the first-run screen (nothing\n * imported yet); `inline` sits inside a card when a filter or a section comes\n * up empty.\n */\nexport function EmptyState({\n  icon: Icon,\n  title,\n  description,\n  action,\n  variant = \"page\",\n  className,\n}: {\n  icon?: LucideIcon;\n  title: string;\n  description?: ReactNode;\n  action?: ReactNode;\n  variant?: \"page\" | \"inline\";\n  className?: string;\n}) {\n  const page = variant === \"page\";\n  return (\n    <div\n      className={cn(\n        \"flex flex-col items-center justify-center text-center\",\n        page ? \"py-16 gap-6\" : \"py-12 gap-3\",\n        className\n      )}\n    >\n      {Icon &&\n        (page ? (\n          <div className=\"h-24 w-24 rounded-full bg-gradient-to-tr from-primary/10 to-primary/30 flex items-center justify-center border border-primary/20 shadow-inner\">\n            <Icon size={38} strokeWidth={1.5} className=\"text-primary/70\" />\n          </div>\n        ) : (\n          <div className=\"h-12 w-12 rounded-full bg-muted flex items-center justify-center\">\n            <Icon size={20} strokeWidth={1.5} className=\"text-muted-foreground\" />\n          </div>\n        ))}\n      <div className={cn(\"max-w-md\", page ? \"space-y-2\" : \"space-y-1\")}>\n        <h2 className={page ? \"text-xl font-bold text-foreground\" : \"text-sm font-medium text-foreground\"}>{title}</h2>\n        {description && <p className={cn(\"text-muted-foreground\", page ? \"text-sm\" : \"text-xs\")}>{description}</p>}\n      </div>\n      {action}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/MonthPicker.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n\n/** Click the month/year straight to any month within a year, instead of\n * stepping one at a time - opens on the trigger's own year, not always the\n * current one, so jumping to a month a year ago doesn't first require\n * paging the popover back itself. */\nexport function MonthPicker({\n  value, onChange, maxDate, label,\n}: {\n  value: Date;\n  onChange: (date: Date) => void;\n  /** No month after this one is selectable - typically \"today\". */\n  maxDate?: Date;\n  label: string;\n}) {\n  const [open, setOpen] = useState(false);\n  const [year, setYear] = useState(value.getFullYear());\n\n  const maxYear = maxDate?.getFullYear();\n  const maxMonth = maxDate?.getMonth();\n\n  return (\n    <Popover\n      open={open}\n      onOpenChange={(o) => {\n        setOpen(o);\n        if (o) setYear(value.getFullYear());\n      }}\n    >\n      <PopoverTrigger asChild>\n        <button\n          type=\"button\"\n          aria-label={label}\n          className=\"text-sm font-medium min-w-[130px] text-center rounded-md hover:bg-background/60 transition-colors px-1 py-1\"\n        >\n          {value.toLocaleDateString(\"en-US\", { month: \"long\", year: \"numeric\" })}\n        </button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-64 p-3\" align=\"center\">\n        <div className=\"flex items-center justify-between mb-2\">\n          <Button\n            variant=\"ghost\" size=\"icon\" className=\"h-7 w-7\"\n            onClick={() => setYear((y) => y - 1)}\n            aria-label=\"Previous year\"\n          >\n            <ChevronLeft size={14} strokeWidth={1.5} />\n          </Button>\n          <span className=\"text-sm font-semibold tabular-nums\">{year}</span>\n          <Button\n            variant=\"ghost\" size=\"icon\" className=\"h-7 w-7\"\n            onClick={() => setYear((y) => y + 1)}\n            disabled={maxYear != null && year >= maxYear}\n            aria-label=\"Next year\"\n          >\n            <ChevronRight size={14} strokeWidth={1.5} />\n          </Button>\n        </div>\n        <div className=\"grid grid-cols-3 gap-1.5\">\n          {MONTHS.map((m, i) => {\n            const isFuture = maxYear != null && maxMonth != null && (year > maxYear || (year === maxYear && i > maxMonth));\n            const isSelected = year === value.getFullYear() && i === value.getMonth();\n            return (\n              <button\n                key={m}\n                type=\"button\"\n                disabled={isFuture}\n                onClick={() => {\n                  onChange(new Date(year, i, 1));\n                  setOpen(false);\n                }}\n                className={cn(\n                  \"text-sm rounded-md py-1.5 transition-colors\",\n                  isSelected\n                    ? \"bg-primary text-primary-foreground font-medium\"\n                    : isFuture\n                      ? \"text-muted-foreground/40 cursor-not-allowed\"\n                      : \"text-foreground hover:bg-muted\"\n                )}\n              >\n                {m}\n              </button>\n            );\n          })}\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/SearchInput.tsx",
      "type": "text/plain",
      "content": "import { forwardRef, useEffect, useImperativeHandle, useRef } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\nfunction isTypingTarget(el: EventTarget | null) {\n  if (!(el instanceof HTMLElement)) return false;\n  return el.isContentEditable || [\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(el.tagName);\n}\n\ninterface Props {\n  value: string;\n  onChange: (value: string) => void;\n  placeholder?: string;\n  className?: string;\n  /** Focus on \"/\" from anywhere on the page (unless the user is typing). */\n  shortcut?: boolean;\n  /** Runs before the shortcut focuses, e.g. to reveal a collapsed filter panel. */\n  onShortcut?: () => void;\n}\n\n/**\n * Search box with a \"/\" shortcut hint. The hint swaps to a clear button once\n * there is a query; Escape clears, and a second Escape leaves the field.\n */\nexport const SearchInput = forwardRef<HTMLInputElement, Props>(function SearchInput(\n  { value, onChange, placeholder = \"Search…\", className, shortcut = true, onShortcut },\n  ref\n) {\n  const inputRef = useRef<HTMLInputElement>(null);\n  useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);\n\n  useEffect(() => {\n    if (!shortcut) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key !== \"/\" || e.metaKey || e.ctrlKey || e.altKey || isTypingTarget(e.target)) return;\n      e.preventDefault();\n      onShortcut?.();\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [shortcut, onShortcut]);\n\n  return (\n    <div className={cn(\"relative\", className)}>\n      <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none\" />\n      <Input\n        ref={inputRef}\n        type=\"search\"\n        placeholder={placeholder}\n        aria-keyshortcuts={shortcut ? \"/\" : undefined}\n        className=\"pl-9 pr-9 bg-background [&::-webkit-search-cancel-button]:hidden\"\n        value={value}\n        onChange={(e) => onChange(e.target.value)}\n        onKeyDown={(e) => {\n          if (e.key !== \"Escape\") return;\n          if (value) onChange(\"\");\n          else inputRef.current?.blur();\n        }}\n      />\n      {value ? (\n        <button\n          type=\"button\"\n          aria-label=\"Clear search\"\n          onClick={() => {\n            onChange(\"\");\n            inputRef.current?.focus();\n          }}\n          className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-1 text-muted-foreground hover:text-foreground\"\n        >\n          <X className=\"h-3.5 w-3.5\" />\n        </button>\n      ) : (\n        shortcut && (\n          <kbd\n            aria-hidden\n            className=\"pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 rounded border border-border bg-muted px-1.5 font-mono text-[10px] leading-4 text-muted-foreground\"\n          >\n            /\n          </kbd>\n        )\n      )}\n    </div>\n  );\n});\n"
    },
    {
      "name": "src/components/elements/SplitEditor.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useState } from \"react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { CategoryPicker } from \"@/components/elements/CategoryPicker\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { UNCATEGORIZED } from \"@/lib/categories\";\nimport type { SplitDto, SplitInput } from \"@/lib/api\";\nimport { Plus, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface Row {\n  category: string;\n  amount: string;\n}\n\nconst emptyRow = (): Row => ({ category: UNCATEGORIZED, amount: \"\" });\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  /** The parent transaction's signed amount; splits target its magnitude. */\n  transactionAmount: number;\n  initialSplits: SplitDto[];\n  onSave: (splits: SplitInput[]) => Promise<unknown>;\n  saving?: boolean;\n}\n\n/**\n * Divides one transaction's amount across several categories. Sends the\n * whole set on save (set_transaction_splits replaces it wholesale), so this\n * always starts from the current splits (or two blank rows for a first-time\n * split) rather than diffing individual add/remove operations.\n */\nexport function SplitEditor({ open, onOpenChange, transactionAmount, initialSplits, onSave, saving }: Props) {\n  const { moneyAbs } = useMoney();\n  const [rows, setRows] = useState<Row[]>([emptyRow(), emptyRow()]);\n  const target = Math.abs(transactionAmount);\n\n  useEffect(() => {\n    if (!open) return;\n    setRows(\n      initialSplits.length > 0\n        ? initialSplits.map((s) => ({ category: s.category || UNCATEGORIZED, amount: String(s.amount) }))\n        : [emptyRow(), emptyRow()]\n    );\n  }, [open, initialSplits]);\n\n  const sum = rows.reduce((n, r) => n + (parseFloat(r.amount) || 0), 0);\n  const remaining = Math.round((target - sum) * 100) / 100;\n  const balanced = Math.abs(remaining) < 0.01;\n  const rowsValid = rows.every((r) => r.category && (parseFloat(r.amount) || 0) > 0);\n  const canSave = rows.length >= 2 && rowsValid && balanced;\n\n  const update = (i: number, patch: Partial<Row>) =>\n    setRows((prev) => prev.map((r, idx) => (idx === i ? { ...r, ...patch } : r)));\n  const addRow = () => setRows((prev) => [...prev, emptyRow()]);\n  const removeRow = (i: number) => setRows((prev) => prev.filter((_, idx) => idx !== i));\n\n  const save = async () => {\n    if (!canSave) return;\n    await onSave(rows.map((r) => ({ category: r.category, amount: Math.round(parseFloat(r.amount) * 100) / 100 })));\n    onOpenChange(false);\n  };\n\n  const removeSplit = async () => {\n    await onSave([]);\n    onOpenChange(false);\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"sm:max-w-[440px]\">\n        <DialogHeader><DialogTitle>Split transaction</DialogTitle></DialogHeader>\n        <div className=\"space-y-3 py-2\">\n          {rows.map((row, i) => (\n            <div key={i} className=\"flex items-center gap-2\">\n              <CategoryPicker\n                value={row.category}\n                onChange={(c) => update(i, { category: c })}\n                triggerClassName=\"h-9 flex-1\"\n              />\n              <Input\n                type=\"number\" min=\"0\" step=\"0.01\" placeholder=\"0.00\"\n                value={row.amount}\n                onChange={(e) => update(i, { amount: e.target.value })}\n                className=\"w-24 shrink-0\"\n              />\n              <Button\n                type=\"button\" variant=\"ghost\" size=\"icon\"\n                className=\"h-8 w-8 shrink-0 text-muted-foreground hover:text-destructive\"\n                aria-label={`Remove row ${i + 1}`}\n                onClick={() => removeRow(i)}\n                disabled={rows.length <= 1}\n              >\n                <X size={14} strokeWidth={1.5} />\n              </Button>\n            </div>\n          ))}\n\n          <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"gap-1.5\" onClick={addRow}>\n            <Plus size={13} strokeWidth={1.75} /> Add split\n          </Button>\n\n          <div className=\"flex items-center justify-between text-sm pt-1 border-t border-border/50\">\n            <span className=\"text-muted-foreground\">Total: {moneyAbs(target)}</span>\n            <span className={cn(\"font-medium\", balanced ? \"text-chart-green\" : \"text-chart-red\")}>\n              {balanced ? \"Balanced\" : remaining > 0 ? `${moneyAbs(remaining)} left` : `${moneyAbs(Math.abs(remaining))} over`}\n            </span>\n          </div>\n        </div>\n        <DialogFooter className=\"flex-row sm:justify-between\">\n          {initialSplits.length > 0 ? (\n            <Button type=\"button\" variant=\"ghost\" className=\"text-destructive hover:text-destructive\" onClick={() => void removeSplit()} disabled={saving}>\n              Remove split\n            </Button>\n          ) : <span />}\n          <div className=\"flex gap-2\">\n            <Button variant=\"outline\" onClick={() => onOpenChange(false)}>Cancel</Button>\n            <Button onClick={() => void save()} disabled={!canSave || saving}>\n              {saving ? \"Saving…\" : \"Save\"}\n            </Button>\n          </div>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/TagFilterPicker.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { Check, ChevronDown, Tag as TagIcon } from \"lucide-react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Command, CommandGroup, CommandItem, CommandList } from \"@/components/ui/command\";\nimport type { TagDto } from \"@/lib/api\";\nimport { tagColorValue } from \"@/lib/tagColors\";\nimport { cn } from \"@/lib/utils\";\n\ninterface Props {\n  /** Every tag the user has defined. */\n  tags: TagDto[];\n  /** Ids of the tags currently filtering the table. Empty = no filter. */\n  selectedIds: string[];\n  onChange: (ids: string[]) => void;\n  disabled?: boolean;\n}\n\n/**\n * Multi-select tag filter: same trigger shape as the other filter Selects in\n * this bar (Category, Type), but opens a checklist instead of a single pick,\n * so \"Business\" and \"Travel\" can narrow the table together instead of one\n * replacing the other. Selecting a row toggles it without closing the\n * popover - TagPicker's own contract, since picking several tags in one go\n * is the whole point of a filter like this.\n */\nexport function TagFilterPicker({ tags, selectedIds, onChange, disabled }: Props) {\n  const [open, setOpen] = useState(false);\n\n  const toggle = (id: string) => {\n    onChange(selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id]);\n  };\n\n  const label =\n    selectedIds.length === 0\n      ? \"All Tags\"\n      : selectedIds.length === 1\n        ? tags.find((t) => t.id === selectedIds[0])?.name ?? \"1 tag\"\n        : `${selectedIds.length} tags`;\n\n  return (\n    <Popover open={open} onOpenChange={setOpen} modal>\n      <PopoverTrigger asChild disabled={disabled}>\n        <button\n          type=\"button\"\n          role=\"combobox\"\n          aria-expanded={open}\n          aria-label=\"Filter by tag\"\n          className=\"flex h-10 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\"\n        >\n          <span className=\"flex items-center gap-2 min-w-0\">\n            <TagIcon size={13} strokeWidth={1.5} className=\"text-muted-foreground shrink-0\" />\n            <span className={cn(\"truncate\", selectedIds.length === 0 && \"text-muted-foreground\")}>{label}</span>\n          </span>\n          <ChevronDown className=\"h-4 w-4 opacity-50 shrink-0\" />\n        </button>\n      </PopoverTrigger>\n      <PopoverContent align=\"start\" className=\"w-[220px] p-0\">\n        <Command>\n          <CommandList className=\"max-h-[300px]\">\n            <CommandGroup>\n              <CommandItem value=\"__all__\" onSelect={() => onChange([])} className=\"gap-2\">\n                <span className=\"flex-1\">All Tags</span>\n                {selectedIds.length === 0 && <Check className=\"h-4 w-4 text-primary\" />}\n              </CommandItem>\n              {tags.map((t) => (\n                <CommandItem key={t.id} value={t.name} onSelect={() => toggle(t.id)} className=\"gap-2\">\n                  <span\n                    className=\"h-2 w-2 rounded-full shrink-0\"\n                    style={{ backgroundColor: tagColorValue(t.color) }}\n                  />\n                  <span className=\"flex-1 truncate\">{t.name}</span>\n                  {selectedIds.includes(t.id) && <Check className=\"h-4 w-4 text-primary\" />}\n                </CommandItem>\n              ))}\n              {tags.length === 0 && <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">No tags yet</div>}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/TagPicker.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { Check, ChevronDown, Plus, Tag as TagIcon, X } from \"lucide-react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from \"@/components/ui/command\";\nimport type { TagDto } from \"@/lib/api\";\nimport { tagColorValue } from \"@/lib/tagColors\";\nimport { cn } from \"@/lib/utils\";\n\nfunction TagDot({ tag }: { tag: TagDto }) {\n  return <span aria-hidden className=\"h-2 w-2 rounded-full shrink-0\" style={{ backgroundColor: tagColorValue(tag.color) }} />;\n}\n\ninterface Props {\n  /** Every tag the user has defined. */\n  tags: TagDto[];\n  /** Ids of the tags currently on this transaction. */\n  selectedIds: string[];\n  onChange: (ids: string[]) => void;\n  /** Creates a new tag (with the default colour) and returns its id, so it\n   * can be selected immediately without a second round trip. */\n  onCreate?: (name: string) => Promise<string>;\n  disabled?: boolean;\n}\n\n/**\n * Multi-select tag picker: selected tags show as small removable pills, the\n * popover list toggles membership instead of replacing a single value like\n * CategoryPicker does. Typing a name with no match offers to create it.\n */\nexport function TagPicker({ tags, selectedIds, onChange, onCreate, disabled }: Props) {\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const [creating, setCreating] = useState(false);\n\n  const selected = tags.filter((t) => selectedIds.includes(t.id));\n  const q = query.trim().toLowerCase();\n  const shown = q ? tags.filter((t) => t.name.toLowerCase().includes(q)) : tags;\n  const exactMatch = tags.some((t) => t.name.toLowerCase() === q);\n\n  const toggle = (id: string) => {\n    onChange(selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id]);\n  };\n\n  const createAndSelect = async () => {\n    if (!onCreate || !query.trim() || creating) return;\n    setCreating(true);\n    try {\n      const id = await onCreate(query.trim());\n      onChange([...selectedIds, id]);\n      setQuery(\"\");\n    } finally {\n      setCreating(false);\n    }\n  };\n\n  return (\n    <div className=\"space-y-2\">\n      {selected.length > 0 && (\n        <div className=\"flex flex-wrap gap-1.5\">\n          {selected.map((t) => (\n            <span\n              key={t.id}\n              className=\"inline-flex items-center gap-1 text-[11px] pl-2 pr-1 py-0.5 rounded-full border bg-muted/60 border-border\"\n            >\n              <TagDot tag={t} />\n              {t.name}\n              <button\n                type=\"button\"\n                aria-label={`Remove ${t.name}`}\n                disabled={disabled}\n                onClick={() => toggle(t.id)}\n                className=\"rounded-full p-0.5 hover:bg-muted-foreground/20 transition-colors\"\n              >\n                <X size={10} strokeWidth={2} />\n              </button>\n            </span>\n          ))}\n        </div>\n      )}\n      <Popover open={open} onOpenChange={(o) => { setOpen(o); if (!o) setQuery(\"\"); }} modal>\n        <PopoverTrigger asChild disabled={disabled}>\n          <button\n            type=\"button\"\n            role=\"combobox\"\n            aria-expanded={open}\n            aria-label=\"Add tags\"\n            className=\"flex h-8 items-center gap-1.5 rounded-md border border-dashed border-input bg-background px-2.5 text-xs text-muted-foreground hover:text-foreground hover:border-foreground/30 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\"\n          >\n            <TagIcon size={12} strokeWidth={1.5} />\n            {selected.length > 0 ? \"Add tag\" : \"Add tags\"}\n            <ChevronDown className=\"h-3 w-3 opacity-50\" />\n          </button>\n        </PopoverTrigger>\n        <PopoverContent align=\"start\" className=\"w-[220px] p-0\">\n          <Command shouldFilter={false}>\n            <CommandInput placeholder=\"Search or create…\" className=\"h-9\" value={query} onValueChange={setQuery} />\n            <CommandList className=\"max-h-[260px]\">\n              {shown.length === 0 && (\n                <CommandEmpty>\n                  {onCreate && query.trim() ? (\n                    <button\n                      type=\"button\"\n                      onClick={() => void createAndSelect()}\n                      disabled={creating}\n                      className={cn(\"flex w-full items-center gap-2 px-2 py-1.5 text-sm text-left rounded-sm hover:bg-accent\", creating && \"opacity-50\")}\n                    >\n                      <Plus size={14} strokeWidth={1.75} />\n                      Create \"{query.trim()}\"\n                    </button>\n                  ) : (\n                    \"No tags found.\"\n                  )}\n                </CommandEmpty>\n              )}\n              <CommandGroup>\n                {shown.map((t) => (\n                  <CommandItem key={t.id} value={t.name} onSelect={() => toggle(t.id)} className=\"gap-2\">\n                    <TagDot tag={t} />\n                    <span className=\"flex-1 truncate\">{t.name}</span>\n                    {selectedIds.includes(t.id) && <Check className=\"h-4 w-4 text-primary\" />}\n                  </CommandItem>\n                ))}\n                {onCreate && query.trim() && !exactMatch && shown.length > 0 && (\n                  <CommandItem value={`__create__${query}`} onSelect={() => void createAndSelect()} className=\"gap-2 text-primary\">\n                    <Plus size={14} strokeWidth={1.75} />\n                    Create \"{query.trim()}\"\n                  </CommandItem>\n                )}\n              </CommandGroup>\n            </CommandList>\n          </Command>\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/elements/categoryIcons.ts",
      "type": "text/plain",
      "content": "import {\n  ArrowLeftRight,\n  Banknote,\n  Car,\n  CircleHelp,\n  Clapperboard,\n  Gift,\n  GraduationCap,\n  HeartPulse,\n  Home,\n  PawPrint,\n  Plane,\n  Receipt,\n  Repeat,\n  Shield,\n  ShoppingBag,\n  ShoppingCart,\n  Sparkles,\n  TrendingUp,\n  Utensils,\n  Zap,\n  type LucideIcon,\n} from \"lucide-react\";\nimport type { Category } from \"@/lib/categories\";\n\n/** One icon per canonical category, shared by avatars, badges and pickers. */\nexport const CATEGORY_ICON: Record<Category, LucideIcon> = {\n  \"Food & Dining\": Utensils,\n  Groceries: ShoppingCart,\n  Transportation: Car,\n  Shopping: ShoppingBag,\n  Entertainment: Clapperboard,\n  Subscriptions: Repeat,\n  \"Bills & Utilities\": Zap,\n  Housing: Home,\n  Health: HeartPulse,\n  Insurance: Shield,\n  Travel: Plane,\n  Education: GraduationCap,\n  \"Personal Care\": Sparkles,\n  \"Gifts & Donations\": Gift,\n  Pets: PawPrint,\n  \"Fees & Charges\": Receipt,\n  Transfers: ArrowLeftRight,\n  Investments: TrendingUp,\n  Income: Banknote,\n  Uncategorized: CircleHelp,\n};\n"
    },
    {
      "name": "src/components/enhancements/CategoryTools.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { registerCustomCategories } from \"@/lib/categories\";\nimport { api } from \"@/lib/api\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { ToolCard, QueryError, useToolAction } from \"./shared\";\n\nexport type CustomCategory = { id: string; name: string; archived: boolean };\nexport function CategoryTools() {\n  const [name, setName] = useState(\"\");\n  const query = useQuery({ queryKey: [\"enhanced\", \"categories\"], queryFn: async () => { const rows = await api.enhanced.get<CustomCategory[]>(\"categories\"); registerCustomCategories(rows.map(r => r.name)); return rows; } });\n  const action = useToolAction();\n  return <ToolCard title=\"Custom categories\">\n    <QueryError error={query.error} />\n    <form className=\"flex gap-2\" onSubmit={e => { e.preventDefault(); action.mutate(async () => { await api.enhanced.post(\"categories\", { name: name.trim() }); setName(\"\"); }); }}>\n      <Input aria-label=\"New category name\" maxLength={60} value={name} onChange={e => setName(e.target.value)} placeholder=\"e.g. Community garden\" required />\n      <Button disabled={!name.trim() || action.isPending}>Add</Button>\n    </form>\n    {query.data?.map(c => <div className=\"flex items-center justify-between gap-2\" key={c.id}><span className=\"text-sm\">{c.name}{c.archived ? \" (archived)\" : \"\"}</span>\n      <Button variant=\"outline\" size=\"sm\" disabled={action.isPending} onClick={() => action.mutate(() => api.enhanced.post(`categories/${c.id}/archive`, { archived: !c.archived }))}>{c.archived ? \"Restore\" : \"Archive\"}</Button></div>)}\n    <p className=\"text-xs text-muted-foreground\">Archiving keeps historical transactions intact and removes the category from new-entry pickers.</p>\n  </ToolCard>;\n}\nexport function RulePreview({ id, onRun, busy }: { id: string; onRun: () => void; busy: boolean }) {\n  const [open, setOpen] = useState(false);\n  const preview = useQuery({ queryKey: [\"enhanced\", \"rule-preview\", id], queryFn: () => api.enhanced.get<{ matched: number; changes: { id: string; description: string; before: string; after: string }[] }>(`rules/${id}/preview`), enabled: open, staleTime: 0 });\n  return <div className=\"space-y-2\">\n    <Button variant=\"outline\" size=\"sm\" onClick={() => setOpen(v => !v)}>{open ? \"Close preview\" : \"Preview\"}</Button>\n    {open && <div className=\"text-sm space-y-2\"><QueryError error={preview.error} />\n      {preview.isPending ? <p>Loading preview…</p> : preview.data && <><p>{preview.data.matched} transactions match. Preview shows up to 100; running applies to all matches.</p>\n        {preview.data.changes.map(c => <p key={c.id}>{c.description}: {c.before} → {c.after}</p>)}\n        <Button size=\"sm\" disabled={busy || preview.data.matched === 0} onClick={onRun}>Apply to matching transactions</Button></>}\n    </div>}\n  </div>;\n}\n"
    },
    {
      "name": "src/components/enhancements/CurrencyTools.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { api } from \"@/lib/api\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { formatMoney, toDateString } from \"@/lib/format\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { ToolCard, QueryError, useToolAction } from \"./shared\";\n\nexport const CURRENCIES = [\"USD\", \"INR\", \"EUR\", \"GBP\", \"CAD\", \"AUD\", \"JPY\", \"CHF\", \"SGD\", \"NZD\", \"AED\", \"HKD\", \"CNY\", \"KRW\", \"MXN\", \"BRL\", \"SEK\", \"NOK\", \"DKK\", \"ZAR\"];\nexport const selectClass = \"h-10 rounded-md border border-input bg-background px-3 text-sm\";\nexport function useAccountCurrencies() {\n  return useQuery({ queryKey: [\"enhanced\", \"currencies\"], queryFn: () => api.enhanced.get<Record<string, string>>(\"currencies\") });\n}\nexport function CurrencyTools() {\n  const { accounts, currency } = useFinanceData();\n  const query = useAccountCurrencies();\n  const action = useToolAction();\n  const [selection, setSelection] = useState<Record<string, string>>({});\n  return <ToolCard title=\"Account currencies\">\n    <QueryError error={query.error} />\n    <p className=\"text-sm text-muted-foreground\">Confirm the unit used by each account. This labels existing amounts without converting them. Confirmed currencies cannot be changed here.</p>\n    {accounts.map(a => <div key={a.id} className=\"flex flex-wrap items-center gap-2\"><span className=\"flex-1 text-sm\">{a.nickname || a.name}</span>\n      {query.data?.[a.id] ? <span className=\"text-sm\">{query.data[a.id]} · confirmed</span> : a.id.startsWith(\"plaid-\") ? <span className=\"text-sm\">Refresh bank sync to confirm currency</span> : <>\n        <select aria-label={`Currency for ${a.name}`} className={selectClass} value={selection[a.id] || currency} onChange={e => setSelection(v => ({ ...v, [a.id]: e.target.value }))}>\n          {CURRENCIES.map(c => <option key={c}>{c}</option>)}\n        </select><Button variant=\"outline\" disabled={!query.data || action.isPending} onClick={() => action.mutate(() => api.enhanced.post(`accounts/${encodeURIComponent(a.id)}/currency`, { currency: selection[a.id] || currency }))}>Confirm currency</Button>\n      </>}\n    </div>)}\n  </ToolCard>;\n}\n\ntype Report = { currency: string | null; accountId: string | null; income: number; expenses: number; net: number; categories: Record<string, number> };\nexport function CurrencyReport() {\n  useMoney(); // Subscribe to the shared privacy preference.\n  const today = new Date();\n  const [start, setStart] = useState(toDateString(new Date(today.getFullYear(), today.getMonth(), 1)));\n  const [end, setEnd] = useState(toDateString(today));\n  const valid = !!start && !!end && start <= end;\n  const report = useQuery({ queryKey: [\"enhanced\", \"currency-report\", start, end], enabled: valid,\n    queryFn: () => api.enhanced.get<Report[]>(`reports?start=${start}&end=${end}`) });\n  return <ToolCard title=\"Spending by currency and split category\">\n    <div className=\"flex flex-wrap gap-3\"><label className=\"text-sm\">From<Input aria-label=\"Report start\" type=\"date\" value={start} onChange={e => setStart(e.target.value)} /></label>\n      <label className=\"text-sm\">Through<Input aria-label=\"Report end\" type=\"date\" value={end} onChange={e => setEnd(e.target.value)} /></label></div>\n    {!valid && <p role=\"alert\">Choose a valid date range.</p>}<QueryError error={report.error} />\n    <p className=\"text-xs text-muted-foreground\">Currencies are reported separately. Splits count against their own categories; internal transfers are excluded. Unconfirmed amounts have no currency label.</p>\n    {report.data?.map(g => <div className=\"space-y-2 border-t pt-3\" key={g.currency || g.accountId}><h3 className=\"font-medium\">{g.currency || `Unconfirmed account: ${g.accountId}`}</h3>\n      {!g.currency ? <p>Confirm account currencies in Settings → Data Sources to include these amounts in monetary totals.</p> : <>\n        <p className=\"text-sm\">In {formatMoney(g.income, g.currency)} · Out {formatMoney(g.expenses, g.currency)} · Net {formatMoney(g.net, g.currency)}</p>\n        {Object.entries(g.categories).map(([c, amount]) => <p className=\"flex justify-between text-sm\" key={c}><span>{c}</span><span>{formatMoney(amount, g.currency)}</span></p>)}\n      </>}\n    </div>)}\n    {!report.isPending && !report.error && report.data?.length === 0 && <p className=\"text-sm\">No dated transactions in this period.</p>}\n  </ToolCard>;\n}\n\ntype Result = { currency: string; expected: number; difference: number; transactionCount: number; hasUndatedTransactions: boolean;\n  transactions: { id: string; date: string; description: string; amount: number }[] };\ntype Saved = { id: string; startDate: string; endDate: string; currency: string; difference: number; stale: boolean };\nexport function ReconciliationTool({ accountId }: { accountId: string }) {\n  const { hidden } = useMoney();\n  const currency = useAccountCurrencies();\n  const [startDate, setStart] = useState(\"\");\n  const [endDate, setEnd] = useState(\"\");\n  const [opening, setOpening] = useState(\"\");\n  const [closing, setClosing] = useState(\"\");\n  const [preview, setPreview] = useState<Result | null>(null);\n  const saved = useQuery({ queryKey: [\"enhanced\", \"reconciliations\", accountId], enabled: !!currency.data?.[accountId], queryFn: () => api.enhanced.get<Saved[]>(`accounts/${encodeURIComponent(accountId)}/reconciliations`) });\n  const action = useToolAction();\n  const payload = { startDate, endDate, opening: Number(opening), closing: Number(closing) };\n  const valid = startDate && endDate && startDate <= endDate && opening !== \"\" && closing !== \"\" && Number.isFinite(Number(opening)) && Number.isFinite(Number(closing));\n  const edit = (fn: () => void) => { fn(); setPreview(null); };\n  return <ToolCard title=\"Reconcile a statement\">\n    {!currency.data?.[accountId] ? <p className=\"text-sm\">Confirm this account's currency in Settings → Data Sources first.</p> : <>\n      <p className=\"text-sm text-muted-foreground\">Compare the statement's opening and closing balances with the ledger. Both dates are inclusive. For credit cards and loans, enter positive balances owed. Saving a comparison does not change your account balance.</p>\n      <div className=\"grid grid-cols-2 gap-3\">\n        <label className=\"text-sm\">Start date<Input type=\"date\" value={startDate} onChange={e => edit(() => setStart(e.target.value))} /></label>\n        <label className=\"text-sm\">End date<Input type=\"date\" value={endDate} onChange={e => edit(() => setEnd(e.target.value))} /></label>\n        <label className=\"text-sm\">Opening ({currency.data[accountId]})<Input type={hidden ? \"password\" : \"number\"} step=\"0.01\" value={opening} onChange={e => edit(() => setOpening(e.target.value))} /></label>\n        <label className=\"text-sm\">Closing ({currency.data[accountId]})<Input type={hidden ? \"password\" : \"number\"} step=\"0.01\" value={closing} onChange={e => edit(() => setClosing(e.target.value))} /></label>\n      </div>\n      <Button variant=\"outline\" disabled={!valid || action.isPending} onClick={() => action.mutate(async () => setPreview(await api.enhanced.post<Result>(`accounts/${encodeURIComponent(accountId)}/reconcile/preview`, payload)))}>Preview comparison</Button>\n      {preview && <div className=\"space-y-2\"><p className=\"text-sm\">Expected {formatMoney(preview.expected, preview.currency)} · Difference {formatMoney(preview.difference, preview.currency)} · {preview.transactionCount} transactions</p>\n        {preview.hasUndatedTransactions && <p role=\"alert\" className=\"text-sm text-destructive\">Undated transactions exist and are excluded. Review their dates before relying on this comparison.</p>}\n        <details><summary className=\"cursor-pointer text-sm\">Review included transactions</summary>{preview.transactions.map(t => <p className=\"text-sm\" key={t.id}>{t.date} · {t.description} · {formatMoney(t.amount, preview.currency)}</p>)}</details>\n        <Button disabled={action.isPending} onClick={() => action.mutate(() => api.enhanced.post(`accounts/${encodeURIComponent(accountId)}/reconcile`, payload))}>Save comparison</Button>\n      </div>}\n      <QueryError error={saved.error} />\n      {saved.data?.map(s => <p className=\"text-sm\" key={s.id}>{s.startDate}–{s.endDate}: difference {formatMoney(s.difference, s.currency)}{s.stale ? \" · ledger changed; compare again\" : \"\"}</p>)}\n    </>}\n  </ToolCard>;\n}\n"
    },
    {
      "name": "src/components/enhancements/PlanningTools.tsx",
      "type": "text/plain",
      "content": "import { useId, useState } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { api } from \"@/lib/api\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { formatMoney, toDateString } from \"@/lib/format\";\nimport { parseLocalDate } from \"@/lib/utils\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { CategoryPicker } from \"@/components/elements/CategoryPicker\";\nimport { DatePicker } from \"@/components/elements/DatePicker\";\nimport { Link } from \"react-router-dom\";\nimport { ToolCard, QueryError, useToolAction } from \"./shared\";\nimport { useAccountCurrencies } from \"./CurrencyTools\";\n\ntype Scheduled = { id: string; title: string; amount: number; currency: string; type: string; category: string; accountId: string; nextDate: string; cadence: string; active: boolean };\ntype Forecast = { basis: string; unconfirmedAccounts: string[]; groups: { currency: string; opening: number; scheduledEnd: number; scenarioEnd: number;\n  events: { date: string; title: string; amount: number; balance: number; overdue: boolean }[] }[] };\n\nexport function PlanningTools({ compact = false }: { compact?: boolean }) {\n  const { accounts } = useFinanceData();\n  const { hidden } = useMoney();\n  const id = useId();\n  const [addOpen, setAddOpen] = useState(false);\n  const [listOpen, setListOpen] = useState(false);\n  const [forecastOpen, setForecastOpen] = useState(false);\n  const currencies = useAccountCurrencies();\n  const schedules = useQuery({ queryKey: [\"enhanced\", \"schedules\"], queryFn: () => api.enhanced.get<Scheduled[]>(\"schedules\") });\n  const [title, setTitle] = useState(\"\");\n  const [amount, setAmount] = useState(\"\");\n  const [accountId, setAccount] = useState(\"\");\n  const [type, setType] = useState(\"expense\");\n  const [category, setCategory] = useState(\"Bills & Utilities\");\n  const [nextDate, setDate] = useState(toDateString(new Date()));\n  const [cadence, setCadence] = useState(\"monthly\");\n  const [extraSpend, setSpend] = useState(\"0\");\n  const [end, setEnd] = useState(toDateString(new Date(Date.now() + 30 * 86400000)));\n  const action = useToolAction();\n  const today = toDateString(new Date());\n  const validScenario = !!end && end >= today && extraSpend !== \"\" && Number.isFinite(Number(extraSpend)) && Number(extraSpend) >= 0;\n  const forecast = useQuery({ queryKey: [\"enhanced\", \"forecast\", today, end, extraSpend], enabled: validScenario && (compact || forecastOpen),\n    queryFn: () => api.enhanced.get<Forecast>(`forecast?start=${today}&end=${end}&extra_spend=${Number(extraSpend)}`) });\n  const available = accounts.filter(a => [\"checking\", \"savings\", \"cash\", \"credit\", \"loan\"].includes(a.type) && currencies.data?.[a.id]);\n  const valid = !!title.trim() && Number(amount) > 0 && Number.isFinite(Number(amount)) && available.some(account => account.id === accountId) && !!nextDate;\n  const forecastResults = <div className=\"space-y-4\">\n    {!validScenario && <p role=\"alert\">Choose a valid period and nonnegative scenario spending.</p>}\n    <QueryError error={forecast.error} />\n    {forecast.isFetching && <p role=\"status\" className=\"text-sm text-muted-foreground\">Updating forecast…</p>}\n    {forecast.data?.unconfirmedAccounts.length ? <p className=\"text-sm\">Confirm the currency of {forecast.data.unconfirmedAccounts.length} accounts in Data Sources to include them.</p> : null}\n    {forecast.data?.groups.map(group => <div className=\"space-y-2 border-t pt-3\" key={group.currency}>\n      <p className=\"text-sm font-medium\">{group.currency} · Current {formatMoney(group.opening, group.currency)} · Scheduled end {formatMoney(group.scheduledEnd, group.currency)}{!compact && <> · Scenario {formatMoney(group.scenarioEnd, group.currency)}</>}</p>\n      {(compact ? group.events.slice(0, 5) : group.events).map((event, i) => <p className=\"flex justify-between gap-3 text-sm\" key={`${event.date}-${i}`}><span>{event.date} · {event.title}{event.overdue ? \" (overdue)\" : \"\"}</span><span>{formatMoney(event.amount, group.currency)}</span></p>)}\n    </div>)}\n    <p className=\"text-xs text-muted-foreground\">{forecast.data?.basis || \"Based on current balances and scheduled items.\"}</p>\n  </div>;\n\n  return <>\n    {!compact && <>\n      <ToolCard title=\"Scheduled bills and income\">\n        <QueryError error={schedules.error} />\n        <p className=\"text-sm text-muted-foreground\">Plan expected bills and income in one place.</p>\n        <p className=\"text-xs text-muted-foreground\">{schedules.data?.length || 0} scheduled items</p>\n        <div className=\"flex flex-wrap gap-2\"><Button onClick={() => setAddOpen(true)}>Add schedule</Button><Button variant=\"outline\" onClick={() => setListOpen(true)}>View scheduled items</Button></div>\n      </ToolCard>\n      <Dialog open={addOpen} onOpenChange={setAddOpen}><DialogContent className=\"max-h-[90dvh] overflow-y-auto sm:max-w-lg\"><DialogHeader><DialogTitle>Add a scheduled bill or income</DialogTitle><DialogDescription>Plan a future transaction. This does not make a bank payment.</DialogDescription></DialogHeader>\n        <QueryError error={currencies.error} />\n        {!accounts.length ? <div className=\"space-y-3\"><p className=\"text-sm text-muted-foreground\">Connect your bank to choose an account for this schedule.</p><Button asChild><Link to=\"/settings/sources?connect=1\">Connect your bank</Link></Button></div> : !available.length ? <p className=\"text-sm text-muted-foreground\">Choose an account currency in Settings → Data Sources to start.</p> : null}\n        <form className=\"space-y-4\" onSubmit={event => { event.preventDefault(); if (valid) action.mutate(async () => {\n          await api.enhanced.post(\"schedules\", { title: title.trim(), amount: Number(amount), type, category, accountId, currency: currencies.data![accountId], nextDate, cadence });\n          setTitle(\"\"); setAmount(\"\"); setAddOpen(false);\n        }); }}>\n          <div className=\"grid gap-4 sm:grid-cols-2\">\n            <div className=\"space-y-2\"><Label htmlFor={`${id}-title`}>Description</Label><Input id={`${id}-title`} value={title} onChange={event => setTitle(event.target.value)} maxLength={120} required /></div>\n            <div className=\"space-y-2\"><Label htmlFor={`${id}-amount`}>Amount</Label><Input id={`${id}-amount`} type={hidden ? \"password\" : \"number\"} inputMode=\"decimal\" min=\"0.01\" step=\"0.01\" value={amount} onChange={event => setAmount(event.target.value)} required /></div>\n            <div className=\"space-y-2\"><Label htmlFor={`${id}-account`}>Account</Label><Select value={accountId} onValueChange={setAccount}><SelectTrigger id={`${id}-account`}><SelectValue placeholder=\"Choose account\" /></SelectTrigger><SelectContent>{available.map(account => <SelectItem key={account.id} value={account.id}>{account.nickname || account.name} ({currencies.data?.[account.id]})</SelectItem>)}</SelectContent></Select></div>\n            <div className=\"space-y-2\"><Label htmlFor={`${id}-direction`}>Direction</Label><Select value={type} onValueChange={value => { setType(value); setCategory(value === \"income\" ? \"Income\" : \"Bills & Utilities\"); }}><SelectTrigger id={`${id}-direction`}><SelectValue /></SelectTrigger><SelectContent><SelectItem value=\"expense\">Expense</SelectItem><SelectItem value=\"income\">Income</SelectItem></SelectContent></Select></div>\n            <div className=\"space-y-2\"><Label>Due date</Label><DatePicker value={nextDate} onChange={value => setDate(value || \"\")} aria-label=\"Due date\" /></div>\n            <div className=\"space-y-2\"><Label htmlFor={`${id}-repeat`}>Repeat</Label><Select value={cadence} onValueChange={setCadence}><SelectTrigger id={`${id}-repeat`}><SelectValue /></SelectTrigger><SelectContent>{[\"once\", \"weekly\", \"fortnightly\", \"monthly\", \"quarterly\", \"yearly\"].map(value => <SelectItem key={value} value={value}>{value.charAt(0).toUpperCase() + value.slice(1)}</SelectItem>)}</SelectContent></Select></div>\n            <div className=\"space-y-2 sm:col-span-2\"><Label>Category</Label><CategoryPicker value={category} onChange={setCategory} /></div>\n          </div>\n          <div className=\"flex justify-end gap-2\"><Button type=\"button\" variant=\"outline\" onClick={() => setAddOpen(false)}>Cancel</Button><Button type=\"submit\" disabled={!valid || action.isPending}>{action.isPending ? \"Saving…\" : \"Save schedule\"}</Button></div>\n        </form>\n      </DialogContent></Dialog>\n      <Dialog open={listOpen} onOpenChange={setListOpen}><DialogContent className=\"max-h-[90dvh] overflow-y-auto sm:max-w-xl\"><DialogHeader><DialogTitle>Scheduled items</DialogTitle><DialogDescription>Skip a payment already in your transactions. Record manually adds a ledger entry without making a bank payment.</DialogDescription></DialogHeader>\n        <QueryError error={schedules.error} />\n        {!schedules.data?.length && <p className=\"py-6 text-sm text-muted-foreground\">No scheduled items yet. Add a bill or income to get started.</p>}\n        {schedules.data?.map(schedule => <div className=\"space-y-3 border-t pt-3\" key={schedule.id}>\n          <div><p className=\"text-sm font-medium\">{schedule.title} · {formatMoney(schedule.amount, schedule.currency)}</p><p className=\"text-xs text-muted-foreground\">{schedule.nextDate} · {schedule.cadence} · {schedule.type}{!schedule.active ? \" · paused / completed\" : schedule.nextDate < today ? \" · overdue\" : \"\"}</p></div>\n          <div className=\"flex flex-wrap gap-2\">{schedule.active && <><Button variant=\"outline\" size=\"sm\" disabled={action.isPending || schedule.nextDate > today} onClick={() => action.mutate(() => api.enhanced.post(`schedules/${schedule.id}/complete`, { date: schedule.nextDate }))}>Record manually</Button><Button variant=\"outline\" size=\"sm\" disabled={action.isPending} onClick={() => action.mutate(() => api.enhanced.post(`schedules/${schedule.id}/complete`, { date: schedule.nextDate, skip: true }))}>Skip</Button></>}\n            <Button variant=\"ghost\" size=\"sm\" disabled={action.isPending} onClick={() => action.mutate(() => api.enhanced.post(`schedules/${schedule.id}/pause`, { archived: schedule.active }))}>{schedule.active ? \"Pause\" : \"Resume\"}</Button></div>\n        </div>)}\n      </DialogContent></Dialog>\n    </>}\n    <ToolCard title={compact ? \"Upcoming scheduled cashflow\" : \"Scheduled cashflow and scenario forecast\"}>\n      {compact ? forecastResults : <><p className=\"text-sm text-muted-foreground\">Explore how upcoming payments and extra spending could affect your balance.</p><Button variant=\"outline\" onClick={() => setForecastOpen(true)}>Explore forecast</Button></>}\n    </ToolCard>\n    {!compact && <Dialog open={forecastOpen} onOpenChange={setForecastOpen}><DialogContent className=\"max-h-[90dvh] overflow-y-auto sm:max-w-xl\"><DialogHeader><DialogTitle>Cashflow forecast</DialogTitle><DialogDescription>Current balances plus scheduled items and your spending scenario. This is an estimate.</DialogDescription></DialogHeader>\n      <div className=\"grid gap-4 sm:grid-cols-2\"><div className=\"space-y-2\"><Label>Forecast through</Label><DatePicker aria-label=\"Forecast through\" value={end} minDate={parseLocalDate(today)} onChange={value => setEnd(value || \"\")} /></div><div className=\"space-y-2\"><Label htmlFor={`${id}-extra`}>Additional spending in each currency</Label><Input id={`${id}-extra`} type={hidden ? \"password\" : \"number\"} min=\"0\" step=\"0.01\" value={extraSpend} onChange={event => setSpend(event.target.value)} /></div></div>\n      {forecastResults}\n    </DialogContent></Dialog>}\n  </>;\n}\n"
    },
    {
      "name": "src/components/enhancements/SecurityTools.tsx",
      "type": "text/plain",
      "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { api } from \"@/lib/api\";\nimport { useAuth } from \"@/context/AuthContext\";\nimport { Button } from \"@/components/ui/button\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { ToolCard, QueryError, useToolAction } from \"./shared\";\n\nexport function SecurityTools() {\n  const sessions = useQuery({ queryKey: [\"sessions\"], queryFn: api.sessions.list });\n  const privacy = useQuery({ queryKey: [\"enhanced\", \"privacy\"], queryFn: () => api.enhanced.get<{ hostedAi: boolean }>(\"privacy\") });\n  const action = useToolAction();\n  const { logout, user } = useAuth();\n  return <>\n    <ToolCard title=\"Signed-in sessions\">\n      <QueryError error={sessions.error} />\n      {sessions.isPending && <p>Loading sessions…</p>}\n      {sessions.data?.map(s => <div key={s.id} className=\"flex items-center justify-between gap-3 border-b pb-2\">\n        <div className=\"min-w-0\"><p className=\"text-sm truncate\">{s.current ? \"This browser\" : s.label}</p><p className=\"text-xs text-muted-foreground\">Expires {new Date(s.expiresAt).toLocaleString()}</p></div>\n        <Button variant=\"outline\" disabled={action.isPending} onClick={() => action.mutate(async () => { if (s.current) { logout(); return; } await api.sessions.revoke(s.id); })}>Sign out</Button>\n      </div>)}\n      <Button variant=\"outline\" disabled={action.isPending || user?.isDemo} onClick={() => action.mutate(async () => { await api.sessions.revokeAll(); logout(); })}>Sign out all sessions</Button>\n      <p className=\"text-xs text-muted-foreground\">API keys remain active until you revoke them separately.</p>\n    </ToolCard>\n    <ToolCard title=\"AI privacy\">\n      <QueryError error={privacy.error} />\n      <label className=\"flex items-center gap-3 text-sm\"><Switch checked={privacy.data?.hostedAi ?? false} disabled={!privacy.data || action.isPending}\n        onCheckedChange={hostedAi => action.mutate(() => api.enhanced.post(\"privacy\", { hostedAi }))} />Allow hosted AI categorization</label>\n      <p className=\"text-sm text-muted-foreground\">Off by default. When enabled, transaction descriptions may be sent to the configured hosted AI provider, including when the local model is unavailable. Local categorization and your rules work without this permission.</p>\n    </ToolCard>\n  </>;\n}\n"
    },
    {
      "name": "src/components/enhancements/SyncTools.tsx",
      "type": "text/plain",
      "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { api } from \"@/lib/api\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { Button } from \"@/components/ui/button\";\nimport { ToolCard, QueryError, useToolAction } from \"./shared\";\n\ntype Status = { provider: string; status: string; attemptedAt: string; succeededAt: string | null; error: string | null; stale: boolean };\nexport function SyncTools() {\n  const query = useQuery({ queryKey: [\"enhanced\", \"sync-status\"], queryFn: () => api.enhanced.get<Status[]>(\"sync-status\"), refetchInterval: 15000 });\n  const { syncPlaid, plaidItems } = useFinanceData();\n  const action = useToolAction();\n  return <ToolCard title=\"Sync health\">\n    <QueryError error={query.error} />\n    {!query.isPending && !query.error && !query.data?.length && <p className=\"text-sm text-muted-foreground\">No sync attempts recorded yet.</p>}\n    {query.data?.map(s => <div key={s.provider} className=\"space-y-1\"><p className=\"text-sm font-medium\">{s.provider}: {s.status}{s.stale ? \" · needs refresh\" : \"\"}</p>\n      <p className=\"text-xs text-muted-foreground\">Last attempt: {new Date(s.attemptedAt).toLocaleString()}. Last complete success: {s.succeededAt ? new Date(s.succeededAt).toLocaleString() : \"none\"}.</p>\n      {s.error && <p role=\"status\" className=\"text-sm\">{s.error}</p>}\n    </div>)}\n    {plaidItems.filter(item => item.sync_error).map(item => <div key={item.item_id} className=\"rounded-md border p-3 text-sm\">\n      <p className=\"font-medium\">{item.institution_name || \"Linked bank\"}: {item.requires_reconnect ? \"Sign-in required\" : \"Sync needs attention\"}</p>\n      <p className=\"mt-1 text-muted-foreground\">{item.sync_error}</p>\n      {item.requires_reconnect && <a href=\"#bank-connections\" className=\"mt-2 inline-block text-primary underline\">Reconnect this bank</a>}\n    </div>)}\n    <Button variant=\"outline\" disabled={action.isPending || !plaidItems.length || query.data?.some(s => s.provider === \"plaid\" && s.status === \"running\")}\n      onClick={() => action.mutate(() => syncPlaid())}>{action.isPending ? \"Syncing…\" : \"Retry bank sync\"}</Button>\n    <p className=\"text-xs text-muted-foreground\">Retries update saved bank records by their provider IDs. A failed source does not remove its saved data.</p>\n    {plaidItems.some(item => item.requires_reconnect) && <p className=\"text-xs text-muted-foreground\">Banks requiring sign-in must be reconnected below before they can sync again.</p>}\n  </ToolCard>;\n}\n"
    },
    {
      "name": "src/components/enhancements/shared.tsx",
      "type": "text/plain",
      "content": "import type { ReactNode } from \"react\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { toast } from \"sonner\";\n\nexport function ToolCard({ title, children }: { title: string; children: ReactNode }) {\n  return <Card className=\"border-border/50\"><CardHeader><CardTitle className=\"text-base\">{title}</CardTitle></CardHeader><CardContent className=\"space-y-4\">{children}</CardContent></Card>;\n}\nexport function QueryError({ error }: { error: unknown }) {\n  return error ? <p role=\"alert\" className=\"text-sm text-destructive\">Couldn't load this tool. Check your connection and try again.</p> : null;\n}\nexport function useToolAction() {\n  const client = useQueryClient();\n  return useMutation({\n    mutationFn: (action: () => Promise<unknown>) => action(),\n    onSuccess: () => { void client.invalidateQueries(); toast.success(\"Saved\"); },\n    onError: (error: Error) => toast.error(error.message || \"Couldn't save. Please retry.\"),\n  });\n}\n"
    },
    {
      "name": "src/components/import/ImportDialog.tsx",
      "type": "text/plain",
      "content": "import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Upload, FileText, AlertTriangle, CheckCircle2, Loader2, ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { toast } from \"sonner\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport {\n  parseCsv, detectMapping, buildTransactions,\n  EMPTY_MAPPING, type ColumnMapping, type ColumnKey, type ImportRow, type ParsedFile,\n} from \"@/lib/import/parse\";\nimport { extractPdfTransactions, pdfRowsAsRecords, PDF_HEADERS } from \"@/lib/import/pdf\";\nimport { CategoryBadge } from \"@/components/elements/CategoryIcon\";\nimport { accountDisplayName, canHoldTransactions } from \"@/lib/accounts\";\n\ntype Step = \"choose\" | \"map\" | \"done\";\n\nconst REQUIRED: ColumnKey[] = [\"date\", \"description\"];\n\nconst FIELD_LABELS: Record<ColumnKey, string> = {\n  date: \"Date\",\n  description: \"Description\",\n  amount: \"Amount\",\n  category: \"Category (optional)\",\n  debit: \"Debit / money out\",\n  credit: \"Credit / money in\",\n};\n\n/**\n * Import transactions from a CSV or bank export.\n *\n * `lib/csv-parser.ts` and `lib/pdf-parser.ts` shipped in this repo, fully\n * written and referenced by nothing, while every empty state in the app invited\n * the user to \"Import Data\" and then offered only Plaid. This is the flow those\n * modules were missing.\n */\nexport function ImportDialog({\n  open,\n  onOpenChange,\n}: {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}) {\n  const { accounts: allAccounts, transactions, addTransactions, refreshData, dateFormat } = useFinanceData();\n  const accounts = useMemo(() => allAccounts.filter((a) => canHoldTransactions(a.type)), [allAccounts]);\n  const { money } = useMoney();\n\n  const [step, setStep] = useState<Step>(\"choose\");\n  const [fileName, setFileName] = useState(\"\");\n  const [parsed, setParsed] = useState<ParsedFile | null>(null);\n  const [mapping, setMapping] = useState<ColumnMapping>(EMPTY_MAPPING);\n  const [accountId, setAccountId] = useState(accounts[0]?.id ?? \"\");\n  const [allPositiveAreExpenses, setAllPositiveAreExpenses] = useState(false);\n  const [skipDuplicates, setSkipDuplicates] = useState(true);\n  const [busy, setBusy] = useState(false);\n  const [dragging, setDragging] = useState(false);\n  const [pdfNote, setPdfNote] = useState(\"\");\n  const [imported, setImported] = useState(0);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  // `accounts` is empty on first render while the context loads, so seeding the\n  // selection from useState's initial value leaves it blank forever, and with\n  // no account the preview can build nothing and the import button never\n  // enables. Adopt the first account as soon as one exists.\n  useEffect(() => {\n    if (!accountId && accounts.length > 0) setAccountId(accounts[0].id);\n  }, [accounts, accountId]);\n\n  const reset = useCallback(() => {\n    setStep(\"choose\");\n    setFileName(\"\");\n    setParsed(null);\n    setMapping(EMPTY_MAPPING);\n    setAllPositiveAreExpenses(false);\n    setSkipDuplicates(true);\n    setImported(0);\n    setPdfNote(\"\");\n    setAccountId(accounts[0]?.id ?? \"\");\n  }, [accounts]);\n\n  const handleFile = useCallback(\n    async (file: File) => {\n      const isPdf = /\\.pdf$/i.test(file.name);\n      if (!isPdf && !/\\.(csv|tsv|txt)$/i.test(file.name)) {\n        toast.error(\"Choose a .csv or .pdf statement\");\n        return;\n      }\n      setBusy(true);\n      try {\n        let result: ParsedFile;\n        if (isPdf) {\n          // PDF statements have no schema, so this is a guess, which is exactly\n          // why it lands on the same preview step for confirmation.\n          const extracted = await extractPdfTransactions(file, dateFormat);\n          if (extracted.rows.length === 0) {\n            toast.error(\"Couldn't find any transactions in that PDF\");\n            return;\n          }\n          result = { headers: PDF_HEADERS, rows: pdfRowsAsRecords(extracted.rows) };\n          setPdfNote(\n            extracted.skipped > 0\n              ? `Read ${extracted.rows.length} of ${extracted.scanned} statement lines, check the preview.`\n              : `Read ${extracted.rows.length} lines, check the preview before importing.`\n          );\n        } else {\n          result = await parseCsv(file);\n          setPdfNote(\"\");\n          if (result.rows.length === 0) {\n            toast.error(\"That file has no rows\");\n            return;\n          }\n        }\n        setParsed(result);\n        setMapping(detectMapping(result.headers));\n        setFileName(file.name);\n        setStep(\"map\");\n      } catch {\n        toast.error(\"Couldn't read that file\");\n      } finally {\n        setBusy(false);\n      }\n    },\n    [dateFormat]\n  );\n\n  const account = accounts.find((a) => a.id === accountId);\n\n  /** Preview is computed live, so changing a mapping updates it immediately. */\n  const rows: ImportRow[] = useMemo(() => {\n    if (!parsed || !account) return [];\n    return buildTransactions(parsed.rows, {\n      mapping,\n      dateFormat,\n      accountId,\n      accountName: accountDisplayName(account),\n      existing: transactions,\n      treatAmountsAsExpenses: allPositiveAreExpenses,\n    });\n  }, [parsed, mapping, dateFormat, accountId, account, transactions, allPositiveAreExpenses]);\n\n  const valid = rows.filter((r) => r.transaction);\n  const failed = rows.filter((r) => !r.transaction);\n  const duplicates = valid.filter((r) => r.duplicate);\n  const toImport = skipDuplicates ? valid.filter((r) => !r.duplicate) : valid;\n\n  const hasAmountSource = !!mapping.amount || !!mapping.debit || !!mapping.credit;\n  const missing = REQUIRED.filter((k) => !mapping[k]);\n  const canImport = missing.length === 0 && hasAmountSource && !!accountId && toImport.length > 0;\n\n  const doImport = async () => {\n    setBusy(true);\n    try {\n      await addTransactions(toImport.map((r) => r.transaction!));\n      setImported(toImport.length);\n      setStep(\"done\");\n      refreshData();\n    } catch {\n      toast.error(\"Import failed\");\n    } finally {\n      setBusy(false);\n    }\n  };\n\n  const close = (o: boolean) => {\n    if (!o) reset();\n    onOpenChange(o);\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={close}>\n      <DialogContent className=\"sm:max-w-[680px] max-h-[86vh] overflow-y-auto\">\n        <DialogHeader>\n          <DialogTitle>\n            {step === \"choose\" && \"Import transactions\"}\n            {step === \"map\" && \"Check the columns\"}\n            {step === \"done\" && \"Import complete\"}\n          </DialogTitle>\n        </DialogHeader>\n\n        {step === \"choose\" && (\n          <div className=\"space-y-4 py-2\">\n            {accounts.length === 0 ? (\n              <p className=\"text-sm text-muted-foreground\">\n                Add or connect an account first, imported transactions need somewhere to live.\n              </p>\n            ) : (\n              <>\n                <div\n                  role=\"button\"\n                  tabIndex={0}\n                  onClick={() => inputRef.current?.click()}\n                  onKeyDown={(e) => (e.key === \"Enter\" || e.key === \" \") && inputRef.current?.click()}\n                  onDragOver={(e) => { e.preventDefault(); setDragging(true); }}\n                  onDragLeave={() => setDragging(false)}\n                  onDrop={(e) => {\n                    e.preventDefault();\n                    setDragging(false);\n                    const f = e.dataTransfer.files?.[0];\n                    if (f) void handleFile(f);\n                  }}\n                  className={cn(\n                    \"border-2 border-dashed rounded-xl p-10 text-center transition-colors cursor-pointer\",\n                    dragging ? \"border-primary bg-primary/5\" : \"border-border hover:border-primary/50\"\n                  )}\n                >\n                  {busy ? (\n                    <Loader2 className=\"h-8 w-8 mx-auto animate-spin text-primary\" />\n                  ) : (\n                    <Upload className=\"h-8 w-8 mx-auto text-muted-foreground\" />\n                  )}\n                  <p className=\"text-sm font-medium text-foreground mt-3\">\n                    Drop a CSV or PDF statement here, or click to choose\n                  </p>\n                  <p className=\"text-xs text-muted-foreground mt-1\">\n                    CSV is most reliable; PDF statements are read best-effort and shown for review\n                  </p>\n                </div>\n                <input\n                  ref={inputRef}\n                  type=\"file\"\n                  accept=\".csv,.tsv,.txt,.pdf,text/csv,application/pdf\"\n                  className=\"sr-only\"\n                  onChange={(e) => {\n                    const f = e.target.files?.[0];\n                    if (f) void handleFile(f);\n                    e.target.value = \"\";\n                  }}\n                />\n              </>\n            )}\n          </div>\n        )}\n\n        {step === \"map\" && parsed && (\n          <div className=\"space-y-5 py-1\">\n            <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n              <FileText size={13} strokeWidth={1.5} />\n              <span className=\"font-medium text-foreground\">{fileName}</span>\n              <span>· {parsed.rows.length} rows</span>\n            </div>\n\n            {pdfNote && (\n              <p className=\"text-xs text-chart-yellow flex items-start gap-1.5\">\n                <AlertTriangle size={12} strokeWidth={1.5} className=\"mt-0.5 shrink-0\" />\n                {pdfNote}\n              </p>\n            )}\n\n            <div className=\"space-y-1.5\">\n              <Label>Import into</Label>\n              <Select value={accountId} onValueChange={setAccountId}>\n                <SelectTrigger><SelectValue placeholder=\"Choose an account\" /></SelectTrigger>\n                <SelectContent>\n                  {accounts.map((a) => (\n                    <SelectItem key={a.id} value={a.id}>{accountDisplayName(a)}</SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n\n            <div className=\"space-y-2\">\n              <Label>Columns</Label>\n              <div className=\"grid grid-cols-2 gap-3\">\n                {(Object.keys(FIELD_LABELS) as ColumnKey[]).map((key) => (\n                  <div key={key} className=\"space-y-1\">\n                    <p className=\"text-xs text-muted-foreground\">\n                      {FIELD_LABELS[key]}\n                      {REQUIRED.includes(key) && <span className=\"text-chart-red ml-0.5\">*</span>}\n                    </p>\n                    <Select\n                      value={mapping[key] || \"__none__\"}\n                      onValueChange={(v) => setMapping((m) => ({ ...m, [key]: v === \"__none__\" ? \"\" : v }))}\n                    >\n                      <SelectTrigger className=\"h-9\"><SelectValue placeholder=\"Not mapped\" /></SelectTrigger>\n                      <SelectContent>\n                        <SelectItem value=\"__none__\">Not mapped</SelectItem>\n                        {parsed.headers.map((h) => <SelectItem key={h} value={h}>{h}</SelectItem>)}\n                      </SelectContent>\n                    </Select>\n                  </div>\n                ))}\n              </div>\n              {!hasAmountSource && (\n                <p className=\"text-xs text-chart-red\">\n                  Map an Amount column, or a Debit/Credit pair.\n                </p>\n              )}\n              <p className=\"text-[11px] text-muted-foreground\">\n                Ambiguous dates like 01/02/2026 are read as {dateFormat}, change that in\n                Settings → Preferences.\n              </p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <label className=\"flex items-center gap-2 text-sm cursor-pointer\">\n                <Checkbox\n                  checked={allPositiveAreExpenses}\n                  onCheckedChange={(c) => setAllPositiveAreExpenses(!!c)}\n                />\n                <span className=\"text-foreground\">Amounts are all positive, treat them as spending</span>\n              </label>\n              <label className=\"flex items-center gap-2 text-sm cursor-pointer\">\n                <Checkbox checked={skipDuplicates} onCheckedChange={(c) => setSkipDuplicates(!!c)} />\n                <span className=\"text-foreground\">\n                  Skip rows that look already imported\n                  {duplicates.length > 0 && (\n                    <span className=\"text-muted-foreground\"> ({duplicates.length} found)</span>\n                  )}\n                </span>\n              </label>\n            </div>\n\n            {/* Preview, what will actually be created. */}\n            <div className=\"space-y-2\">\n              <div className=\"flex items-center justify-between\">\n                <Label>Preview</Label>\n                <div className=\"flex items-center gap-2 text-xs\">\n                  <Badge variant=\"secondary\" className=\"bg-chart-green/10 text-chart-green\">\n                    {toImport.length} to import\n                  </Badge>\n                  {duplicates.length > 0 && (\n                    <Badge variant=\"secondary\" className=\"bg-chart-yellow/10 text-chart-yellow\">\n                      {duplicates.length} duplicate{duplicates.length === 1 ? \"\" : \"s\"}\n                    </Badge>\n                  )}\n                  {failed.length > 0 && (\n                    <Badge variant=\"secondary\" className=\"bg-chart-red/10 text-chart-red\">\n                      {failed.length} unreadable\n                    </Badge>\n                  )}\n                </div>\n              </div>\n\n              <div className=\"border border-border/50 rounded-lg overflow-hidden max-h-56 overflow-y-auto\">\n                {toImport.slice(0, 40).map((r) => (\n                  <div\n                    key={r.index}\n                    className=\"grid grid-cols-[92px_1fr_auto] gap-2 items-center px-3 py-2 text-xs border-b border-border/20 last:border-0\"\n                  >\n                    <span className=\"text-muted-foreground tabular-nums\">{r.transaction!.date}</span>\n                    <span className=\"truncate text-foreground\" title={r.transaction!.description}>\n                      {r.transaction!.description}\n                    </span>\n                    <span className=\"flex items-center gap-2 shrink-0\">\n                      <CategoryBadge category={r.transaction!.category} className=\"px-1.5 text-[10px]\" />\n                      <span className={cn(\"tabular-nums font-medium\", r.transaction!.amount < 0 ? \"text-foreground\" : \"text-chart-green\")}>\n                        {money(r.transaction!.amount)}\n                      </span>\n                    </span>\n                  </div>\n                ))}\n                {toImport.length === 0 && (\n                  <p className=\"text-xs text-muted-foreground text-center py-6\">\n                    Nothing to import with these settings.\n                  </p>\n                )}\n              </div>\n\n              {/* Failures are listed rather than silently dropped, so a\n                  mis-mapped column is obvious instead of looking like data loss. */}\n              {failed.length > 0 && (\n                <details className=\"text-xs\">\n                  <summary className=\"cursor-pointer text-muted-foreground hover:text-foreground flex items-center gap-1.5\">\n                    <AlertTriangle size={12} strokeWidth={1.5} className=\"text-chart-yellow\" />\n                    {failed.length} row{failed.length === 1 ? \"\" : \"s\"} couldn't be read\n                  </summary>\n                  <div className=\"mt-2 space-y-1 max-h-28 overflow-y-auto\">\n                    {failed.slice(0, 20).map((r) => (\n                      <p key={r.index} className=\"text-muted-foreground\">\n                        Row {r.index + 2}: {r.error}\n                      </p>\n                    ))}\n                  </div>\n                </details>\n              )}\n            </div>\n          </div>\n        )}\n\n        {step === \"done\" && (\n          <div className=\"py-8 text-center space-y-3\">\n            <CheckCircle2 className=\"h-12 w-12 mx-auto text-chart-green\" />\n            <p className=\"text-lg font-semibold text-foreground\">\n              Imported {imported} transaction{imported === 1 ? \"\" : \"s\"}\n            </p>\n            <p className=\"text-sm text-muted-foreground\">\n              They're in your transaction list and counted towards your budgets.\n            </p>\n          </div>\n        )}\n\n        <DialogFooter className=\"sm:justify-between\">\n          {step === \"map\" ? (\n            <Button variant=\"ghost\" className=\"gap-1.5\" onClick={() => setStep(\"choose\")}>\n              <ArrowLeft size={14} strokeWidth={1.5} /> Back\n            </Button>\n          ) : <span />}\n          <div className=\"flex gap-2\">\n            <Button variant=\"outline\" onClick={() => close(false)}>\n              {step === \"done\" ? \"Close\" : \"Cancel\"}\n            </Button>\n            {step === \"map\" && (\n              <Button onClick={() => void doImport()} disabled={!canImport || busy}>\n                {busy ? \"Importing…\" : `Import ${toImport.length}`}\n              </Button>\n            )}\n            {step === \"done\" && <Button onClick={reset}>Import another</Button>}\n          </div>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/AccountMenu.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { MoreVertical, Pencil, Tag as TagIcon, Trash2, Unlink } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { TagPicker } from \"@/components/elements/TagPicker\";\nimport { useTags, useTagMutations, useInvestmentAccountTags } from \"@/hooks/useTags\";\nimport { api } from \"@/lib/api\";\nimport { toast } from \"sonner\";\n\ninterface Props {\n  accountId: string;\n  accountLabel: string;\n  isManual: boolean;\n  onRenamed: () => void;\n  onDeleted: () => void;\n}\n\n/**\n * Rename, tag, and delete/disconnect for an investment account, manual or\n * connected alike - renaming and tagging already work identically for both\n * server-side (PATCH /investment-accounts/{id} has no provider check), only\n * \"Delete\" vs \"Disconnect\" reads differently since a connected account has a\n * live sync to sever rather than data to simply remove.\n */\nexport function AccountMenu({ accountId, accountLabel, isManual, onRenamed, onDeleted }: Props) {\n  const [renameOpen, setRenameOpen] = useState(false);\n  const [tagsOpen, setTagsOpen] = useState(false);\n  const [deleteOpen, setDeleteOpen] = useState(false);\n  const [newLabel, setNewLabel] = useState(accountLabel);\n  const [saving, setSaving] = useState(false);\n\n  const { data: allTags } = useTags();\n  const { create } = useTagMutations();\n  const { data: accountTags, setTags } = useInvestmentAccountTags(tagsOpen ? accountId : undefined);\n\n  const handleRename = async (e: React.FormEvent) => {\n    e.preventDefault();\n    const trimmed = newLabel.trim();\n    if (!trimmed || trimmed === accountLabel) {\n      setRenameOpen(false);\n      return;\n    }\n    setSaving(true);\n    try {\n      await api.investments.rename(accountId, trimmed);\n      onRenamed();\n      setRenameOpen(false);\n      toast.success(\"Account renamed\");\n    } catch {\n      toast.error(\"Failed to rename account\");\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  const handleDelete = async () => {\n    try {\n      await api.investments.disconnect(accountId);\n      onDeleted();\n      toast.success(isManual ? \"Portfolio deleted\" : \"Account disconnected\");\n    } catch {\n      toast.error(isManual ? \"Failed to delete portfolio\" : \"Failed to disconnect account\");\n    }\n  };\n\n  return (\n    <>\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          {/* Stop propagation so clicking the menu on a card/link doesn't navigate */}\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            className=\"h-7 w-7 shrink-0\"\n            onClick={(e) => e.preventDefault()}\n          >\n            <MoreVertical size={15} strokeWidth={1.5} />\n            <span className=\"sr-only\">Account actions</span>\n          </Button>\n        </DropdownMenuTrigger>\n        {/* React portals bubble events through the REACT tree, not the DOM\n            tree: even though this content renders into document.body (a\n            Radix Portal), a click on one of its items still bubbles, as a\n            React synthetic event, up through this component to the parent\n            row's <Link>, navigating it before the dialog we opened ever\n            shows. Stopping it here, once, covers every item below. */}\n        <DropdownMenuContent align=\"end\" className=\"w-40\" onClick={(e) => e.stopPropagation()}>\n          <DropdownMenuItem\n            onSelect={() => {\n              setNewLabel(accountLabel);\n              setRenameOpen(true);\n            }}\n          >\n            <Pencil size={13} className=\"mr-2\" />\n            Rename\n          </DropdownMenuItem>\n          <DropdownMenuItem onSelect={() => setTagsOpen(true)}>\n            <TagIcon size={13} className=\"mr-2\" />\n            Tags\n          </DropdownMenuItem>\n          <DropdownMenuSeparator />\n          <DropdownMenuItem\n            onSelect={() => setDeleteOpen(true)}\n            className=\"text-destructive focus:text-destructive\"\n          >\n            {isManual ? <Trash2 size={13} className=\"mr-2\" /> : <Unlink size={13} className=\"mr-2\" />}\n            {isManual ? \"Delete\" : \"Disconnect\"}\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n\n      {/* Rename dialog */}\n      <Dialog open={renameOpen} onOpenChange={setRenameOpen}>\n        <DialogContent className=\"sm:max-w-[360px]\" onClick={(e) => e.stopPropagation()}>\n          <DialogHeader>\n            <DialogTitle>Rename account</DialogTitle>\n          </DialogHeader>\n          <form onSubmit={handleRename} className=\"space-y-4 pt-2\">\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"rename-label\">Name</Label>\n              <Input\n                id=\"rename-label\"\n                value={newLabel}\n                onChange={(e) => setNewLabel(e.target.value)}\n                autoFocus\n                required\n              />\n            </div>\n            <div className=\"flex justify-end gap-2\">\n              <Button type=\"button\" variant=\"outline\" onClick={() => setRenameOpen(false)}>\n                Cancel\n              </Button>\n              <Button type=\"submit\" disabled={saving}>\n                {saving ? \"Saving…\" : \"Save\"}\n              </Button>\n            </div>\n          </form>\n        </DialogContent>\n      </Dialog>\n\n      {/* Tags dialog */}\n      <Dialog open={tagsOpen} onOpenChange={setTagsOpen}>\n        <DialogContent className=\"sm:max-w-[360px]\" onClick={(e) => e.stopPropagation()}>\n          <DialogHeader>\n            <DialogTitle>Tag \"{accountLabel}\"</DialogTitle>\n          </DialogHeader>\n          <div className=\"pt-2\">\n            <TagPicker\n              tags={allTags ?? []}\n              selectedIds={(accountTags ?? []).map((t) => t.id)}\n              onChange={(ids) => setTags.mutate(ids)}\n              onCreate={async (name) => {\n                const created = await create.mutateAsync({ name });\n                return created.id;\n              }}\n            />\n          </div>\n        </DialogContent>\n      </Dialog>\n\n      {/* Delete/disconnect confirmation */}\n      <AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>\n        <AlertDialogContent onClick={(e) => e.stopPropagation()}>\n          <AlertDialogHeader>\n            <AlertDialogTitle>{isManual ? \"Delete\" : \"Disconnect\"} {accountLabel}?</AlertDialogTitle>\n            <AlertDialogDescription>\n              {isManual\n                ? \"This removes the portfolio and all its holdings. This action cannot be undone.\"\n                : \"This severs the connection to this account. Holdings already synced stay in your history.\"}\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>Cancel</AlertDialogCancel>\n            <AlertDialogAction\n              className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n              onClick={handleDelete}\n            >\n              {isManual ? \"Delete\" : \"Disconnect\"}\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/AddInvestmentAccountButton.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n} from \"@/components/ui/dropdown-menu\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { useAuth } from \"@/context/AuthContext\";\nimport { api } from \"@/lib/api\";\nimport { Plus, Loader2, Building2, PencilLine } from \"lucide-react\";\nimport { toast } from \"sonner\";\n\ninterface Props {\n  size?: \"sm\" | \"default\";\n  label?: string;\n  className?: string;\n}\n\n/**\n * Provider picker for adding an investment source: Kite (OAuth) and a Manual portfolio (created on the spot, then navigated to).\n * Shared by the Brokerages settings page and the Investments overview.\n */\nexport function AddInvestmentAccountButton({ size = \"default\", label = \"Add account\", className }: Props) {\n  const { investmentAccounts, refreshData } = useFinanceData();\n  const { user } = useAuth();\n  const navigate = useNavigate();\n  const [busy, setBusy] = useState(false);\n\n  const connectKite = async () => {\n    setBusy(true);\n    try {\n      const { login_url } = await api.investments.connectUrl(\"kite\");\n      const w = 520, h = 740;\n      const left = Math.max(0, Math.floor((window.screen.width - w) / 2));\n      const top = Math.max(0, Math.floor((window.screen.height - h) / 4));\n      const popup = window.open(login_url, \"investment_oauth\", `popup=yes,width=${w},height=${h},left=${left},top=${top}`);\n      if (!popup) window.location.href = login_url;\n    } catch (err) {\n      toast.error(\"Failed to get login URL: \" + ((err as Error)?.message || \"Unknown error\"));\n    } finally {\n      setBusy(false);\n    }\n  };\n\n  const addManual = async () => {\n    setBusy(true);\n    try {\n      // Default the portfolio currency to an existing account's (e.g. an INR Kite\n      // account) so manual holdings line up; backend falls back to USD.\n      const currency = investmentAccounts[0]?.currency ?? \"USD\";\n      const acct = await api.investments.createManual(\"Manual Portfolio\", currency);\n      refreshData();\n      toast.success(\"Manual portfolio ready, add your holdings\");\n      navigate(`/investments/${acct.id}`);\n    } catch (err) {\n      toast.error(\"Failed to create manual portfolio: \" + ((err as Error)?.message || \"Unknown error\"));\n    } finally {\n      setBusy(false);\n    }\n  };\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button size={size} disabled={busy} className={className ?? \"gap-2\"}>\n          {busy ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Plus className=\"h-4 w-4\" />}\n          {label}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"w-56\">\n        <DropdownMenuLabel>Add investment source</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        {user?.kiteEnabled && (\n          <DropdownMenuItem onClick={connectKite} className=\"gap-2\">\n            <Building2 className=\"h-4 w-4\" /> Kite (Zerodha)\n          </DropdownMenuItem>\n        )}\n        <DropdownMenuItem onClick={addManual} className=\"gap-2\">\n          <PencilLine className=\"h-4 w-4\" /> Manual portfolio\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/AddInvestmentDialog.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { format } from \"date-fns\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Button } from \"@/components/ui/button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Calendar as CalendarUI } from \"@/components/ui/calendar\";\nimport { Plus, Calendar as CalendarIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type { Holding, HistoricalValue } from \"@/types/finance\";\nimport { ASSET_CATEGORIES } from \"@/lib/holdings\";\n\nexport function AddInvestmentDialog({ onAdd, defaultCurrency }: { onAdd: (h: Holding) => void; defaultCurrency: string }) {\n  const [open, setOpen] = useState(false);\n  const [name, setName] = useState(\"\");\n  const [category, setCategory] = useState(\"Private Equity\");\n  const [currency, setCurrency] = useState(defaultCurrency);\n  const [details, setDetails] = useState(\"\");\n  const [amount, setAmount] = useState(\"\");\n  const [currentValue, setCurrentValue] = useState(\"\");\n  const [ticker, setTicker] = useState(\"\");\n  const [quantity, setQuantity] = useState(\"1\");\n  const [dateObj, setDateObj] = useState<Date>(new Date());\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    if (!name || !amount || !dateObj) return;\n\n    const dateStr = format(dateObj, \"yyyy-MM-dd\");\n    const costBasis = parseFloat(amount) || 0;\n    const price = currentValue ? parseFloat(currentValue) : costBasis;\n    const shares = parseFloat(quantity) > 0 ? parseFloat(quantity) : 1;\n\n    const history: HistoricalValue[] = [{ date: dateStr, value: costBasis }];\n    const today = format(new Date(), \"yyyy-MM-dd\");\n    if (price !== costBasis && dateStr !== today) {\n      history.push({ date: today, value: price });\n    }\n\n    const derived = name.length > 4 ? name.substring(0, 4).toUpperCase() : name.toUpperCase();\n    onAdd({\n      id: `manual_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,\n      name,\n      // A linked public ticker enables live tracking; else a name-derived placeholder.\n      symbol: ticker.trim() ? ticker.trim().toUpperCase() : derived,\n      shares,\n      price,\n      costBasis,\n      change: price - costBasis,\n      changePercent: costBasis > 0 ? ((price - costBasis) / costBasis) * 100 : 0,\n      currency,\n      isManual: true,\n      category,\n      details,\n      dateInvested: dateStr,\n      history,\n    });\n    setOpen(false);\n    setName(\"\"); setDetails(\"\"); setAmount(\"\"); setCurrentValue(\"\"); setTicker(\"\"); setQuantity(\"1\");\n    setDateObj(new Date());\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button size=\"sm\" className=\"gap-2 shadow-sm whitespace-nowrap\"><Plus className=\"h-4 w-4\" /> Add Asset</Button>\n      </DialogTrigger>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader>\n          <DialogTitle>Add Manual Investment</DialogTitle>\n        </DialogHeader>\n        <form onSubmit={handleSubmit} className=\"space-y-4 pt-4\">\n          <div className=\"grid grid-cols-2 gap-4\">\n            <div className=\"space-y-2\">\n              <Label>Asset Name</Label>\n              <Input placeholder=\"e.g. SpaceX Series A\" value={name} onChange={e => setName(e.target.value)} required />\n            </div>\n            <div className=\"space-y-2\">\n              <Label>Category</Label>\n              <Select value={category} onValueChange={setCategory}>\n                <SelectTrigger><SelectValue /></SelectTrigger>\n                <SelectContent>\n                  {ASSET_CATEGORIES.map(c => <SelectItem key={c} value={c}>{c}</SelectItem>)}\n                </SelectContent>\n              </Select>\n            </div>\n            <div className=\"space-y-2 flex flex-col pt-0.5\">\n              <Label className=\"mb-1\">Investment Date</Label>\n              <Popover>\n                <PopoverTrigger asChild>\n                  <Button variant=\"outline\" className={cn(\"w-full justify-start text-left font-normal\", !dateObj && \"text-muted-foreground\")}>\n                    <CalendarIcon className=\"mr-2 h-4 w-4\" />\n                    {dateObj ? format(dateObj, \"PPP\") : \"Pick a date\"}\n                  </Button>\n                </PopoverTrigger>\n                <PopoverContent className=\"w-auto p-0 z-[100]\" align=\"start\">\n                  <CalendarUI mode=\"single\" selected={dateObj} onSelect={(d) => d && setDateObj(d)} initialFocus />\n                </PopoverContent>\n              </Popover>\n            </div>\n            <div className=\"space-y-2\">\n              <Label>Currency</Label>\n              <Select value={currency} onValueChange={setCurrency}>\n                <SelectTrigger><SelectValue /></SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"USD\">USD</SelectItem>\n                  <SelectItem value=\"INR\">INR</SelectItem>\n                  <SelectItem value=\"EUR\">EUR</SelectItem>\n                  <SelectItem value=\"GBP\">GBP</SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n            <div className=\"space-y-2\">\n              <Label>Avg cost / share</Label>\n              <Input type=\"number\" step=\"0.01\" placeholder=\"Cost per share\" value={amount} onChange={e => setAmount(e.target.value)} required />\n            </div>\n            <div className=\"space-y-2\">\n              <Label>Quantity / Shares</Label>\n              <Input type=\"number\" step=\"0.0001\" min=\"0.0001\" placeholder=\"1\" value={quantity} onChange={e => setQuantity(e.target.value)} />\n            </div>\n            <div className=\"space-y-2\">\n              <Label>Current Value</Label>\n              <Input type=\"number\" step=\"0.01\" placeholder=\"Leave empty if unchanged\" value={currentValue} onChange={e => setCurrentValue(e.target.value)} />\n            </div>\n            <div className=\"space-y-2 col-span-2\">\n              <Label>Public ticker (optional)</Label>\n              <Input placeholder=\"e.g. AAPL, RELIANCE, set once public for live tracking\" value={ticker} onChange={e => setTicker(e.target.value)} />\n              <p className=\"text-xs text-muted-foreground\">\n                For a public ticker, enter avg cost in its trading currency (e.g. USD for SPCX). The live price will display in that native currency.\n              </p>\n            </div>\n            <div className=\"space-y-2 col-span-2\">\n              <Label>Additional Details / Notes</Label>\n              <Input placeholder=\"e.g. Purchased through Securitize\" value={details} onChange={e => setDetails(e.target.value)} />\n            </div>\n          </div>\n          <Button type=\"submit\" className=\"w-full\">Track Asset</Button>\n        </form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/AllocationPanel.tsx",
      "type": "text/plain",
      "content": "import { useMemo, useState } from \"react\";\nimport { Card, CardContent, CardHeader, CardTitle, CardDescription } from \"@/components/ui/card\";\nimport { DonutChart } from \"@/components/charts/DonutChart\";\nimport { BubbleChart } from \"@/components/charts/BubbleChart\";\nimport { ChartTypeToggle } from \"@/components/charts/ChartTypeToggle\";\nimport { SegmentedControl } from \"@/components/charts/SegmentedControl\";\nimport { shareColor } from \"@/components/dashboard/shareColors\";\nimport { allocationBreakdown, type AllocationMode } from \"@/lib/holdings\";\nimport { usePrivacy } from \"@/lib/privacy\";\nimport { formatMoney } from \"@/lib/format\";\nimport type { Holding } from \"@/types/finance\";\nimport { PieChart, Circle } from \"lucide-react\";\n\n// \"currency\" is deliberately not offered here: this panel receives holdings\n// already scoped to one currency (the caller filters, same as the headline\n// stats), so a currency breakdown of them would always be a single 100% slice.\nconst MODES: { value: AllocationMode; label: string }[] = [\n  { value: \"asset\", label: \"Asset\" },\n  { value: \"account\", label: \"Account\" },\n];\nconst CHART_TYPES = [\n  { value: \"donut\", icon: PieChart, label: \"Donut\" },\n  { value: \"bubbles\", icon: Circle, label: \"Bubbles\" },\n] as const;\n\nfunction fmt(v: number, c: string) {\n  return formatMoney(v, c, { decimals: 0 });\n}\n\nexport function AllocationPanel({\n  holdings, investmentAccounts, currency,\n}: {\n  holdings: Holding[];\n  investmentAccounts: { id: string; label: string }[];\n  currency: string;\n}) {\n  usePrivacy();\n  const [mode, setMode] = useState<AllocationMode>(\"asset\");\n  const [chartType, setChartType] = useState<\"donut\" | \"bubbles\">(\"donut\");\n\n  const rows = useMemo(() => allocationBreakdown(holdings, mode, investmentAccounts), [holdings, mode, investmentAccounts]);\n  const total = rows.reduce((s, r) => s + r.value, 0);\n  const slices = rows.map((r, i) => ({ name: r.label, value: r.value, color: shareColor(i) }));\n\n  return (\n    <Card className=\"border-border/50\">\n      <CardHeader className=\"pb-3\">\n        <div className=\"flex items-start justify-between gap-3\">\n          <div>\n            <CardTitle className=\"text-base font-semibold\">Allocation</CardTitle>\n          </div>\n          <ChartTypeToggle options={CHART_TYPES} value={chartType} onChange={setChartType} label=\"Chart type\" />\n        </div>\n        <SegmentedControl label=\"Group by\" options={MODES} value={mode} onChange={setMode} className=\"mt-1\" />\n      </CardHeader>\n      <CardContent>\n        {rows.length === 0 ? (\n          <p className=\"text-sm text-muted-foreground text-center py-8\">No holdings to allocate yet.</p>\n        ) : (\n          <div className=\"flex flex-col sm:flex-row items-center gap-6\">\n            {chartType === \"donut\" ? (\n              <DonutChart\n                data={slices}\n                size={160}\n                thickness={26}\n                formatValue={(v) => fmt(v, currency)}\n                centerLabel={`${rows.length} ${rows.length === 1 ? \"group\" : \"groups\"}`}\n                centerValue={fmt(total, currency)}\n              />\n            ) : (\n              <BubbleChart data={slices} size={160} formatValue={(v) => fmt(v, currency)} />\n            )}\n            <div className=\"flex-1 w-full space-y-2\">\n              {rows.map((r, i) => {\n                const pct = total > 0 ? (r.value / total) * 100 : 0;\n                return (\n                  <div key={r.key} className=\"flex items-center justify-between gap-2 text-sm\">\n                    <span className=\"flex items-center gap-2 min-w-0\">\n                      <span aria-hidden className=\"h-2.5 w-2.5 rounded-full shrink-0\" style={{ backgroundColor: shareColor(i) }} />\n                      <span className=\"text-foreground truncate\">{r.label}</span>\n                    </span>\n                    <span className=\"text-muted-foreground tabular-nums shrink-0\">{pct.toFixed(1)}%</span>\n                  </div>\n                );\n              })}\n            </div>\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/ConnectedAccountsList.tsx",
      "type": "text/plain",
      "content": "import { useMemo, useState } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { SegmentedControl } from \"@/components/charts/SegmentedControl\";\nimport { shareColor } from \"@/components/dashboard/shareColors\";\nimport { useAllInvestmentAccountTags } from \"@/hooks/useTags\";\nimport { tagColorValue } from \"@/lib/tagColors\";\nimport { usePrivacy } from \"@/lib/privacy\";\nimport { formatMoney } from \"@/lib/format\";\nimport { cn } from \"@/lib/utils\";\nimport type { TagDto } from \"@/lib/api\";\n\nexport interface ConnectedAccountRow {\n  id: string;\n  label: string;\n  to: string;\n  value: number;\n  pct: number;\n  currency: string;\n  /** The account's own currency, when its value is shown converted into another. */\n  nativeCurrency?: string;\n  connected: boolean;\n  isManual: boolean;\n  actions?: React.ReactNode;\n}\n\ntype SortMode = \"value\" | \"name\";\nconst SORT_OPTIONS: { value: SortMode; label: string }[] = [\n  { value: \"value\", label: \"Value\" },\n  { value: \"name\", label: \"Name\" },\n];\n\nfunction fmt(v: number, c: string) {\n  return formatMoney(v, c, { decimals: 0 });\n}\n\nfunction initials(label: string): string {\n  const words = label.trim().split(/\\s+/).filter(Boolean);\n  if (words.length === 0) return \"?\";\n  if (words.length === 1) return words[0].slice(0, 2).toUpperCase();\n  return (words[0][0] + words[1][0]).toUpperCase();\n}\n\n/**\n * Ranked, information-dense list of every connected/manual source, replacing\n * a grid of big cards: at a glance, which source is biggest and what share\n * of the total it is, the two things a card forces you to compare by eye.\n *\n * Sort (Value/Name) and an optional tag filter give two independent ways to\n * organize the list once it's got more than a handful of accounts in it.\n */\nexport function ConnectedAccountsList({ accounts, total, currency }: { accounts: ConnectedAccountRow[]; total: number; currency: string }) {\n  usePrivacy();\n  const [sortMode, setSortMode] = useState<SortMode>(\"value\");\n  const [activeTagId, setActiveTagId] = useState<string | null>(null);\n\n  const accountIds = useMemo(() => accounts.map((a) => a.id), [accounts]);\n  const tagsById = useAllInvestmentAccountTags(accountIds);\n\n  const allTags = useMemo(() => {\n    const seen = new Map<string, TagDto>();\n    Object.values(tagsById).forEach((tags) => tags.forEach((t) => seen.set(t.id, t)));\n    return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));\n  }, [tagsById]);\n\n  const filtered = activeTagId\n    ? accounts.filter((a) => (tagsById[a.id] ?? []).some((t) => t.id === activeTagId))\n    : accounts;\n\n  const sorted = useMemo(() => {\n    const rows = [...filtered];\n    if (sortMode === \"name\") rows.sort((a, b) => a.label.localeCompare(b.label));\n    else rows.sort((a, b) => b.value - a.value);\n    return rows;\n  }, [filtered, sortMode]);\n\n  return (\n    <Card className=\"border-border/50\">\n      <CardHeader className=\"pb-3 space-y-3\">\n        <div className=\"flex items-center justify-between\">\n          <CardTitle className=\"text-base font-semibold\">Connected accounts</CardTitle>\n          <span className=\"text-2xl font-bold text-foreground tabular-nums\">{accounts.length}</span>\n        </div>\n        {accounts.length > 1 && (\n          <div className=\"flex items-center justify-between gap-2\">\n            <SegmentedControl label=\"Sort by\" options={SORT_OPTIONS} value={sortMode} onChange={setSortMode} />\n          </div>\n        )}\n        {allTags.length > 0 && (\n          <div className=\"flex flex-wrap items-center gap-1.5\">\n            {allTags.map((t) => {\n              const active = activeTagId === t.id;\n              return (\n                <button\n                  key={t.id}\n                  type=\"button\"\n                  onClick={() => setActiveTagId(active ? null : t.id)}\n                  className={cn(\n                    \"inline-flex items-center gap-1 text-[11px] pl-1.5 pr-2 py-0.5 rounded-full border transition-colors\",\n                    active ? \"border-foreground bg-muted\" : \"border-border bg-muted/40 hover:bg-muted/70\"\n                  )}\n                >\n                  <span aria-hidden className=\"h-1.5 w-1.5 rounded-full shrink-0\" style={{ backgroundColor: tagColorValue(t.color) }} />\n                  {t.name}\n                </button>\n              );\n            })}\n          </div>\n        )}\n      </CardHeader>\n      <CardContent className=\"p-0 divide-y divide-border/30\">\n        {sorted.length === 0 ? (\n          <p className=\"text-sm text-muted-foreground text-center py-8 px-6\">\n            {activeTagId ? \"No accounts with that tag.\" : \"No accounts yet.\"}\n          </p>\n        ) : (\n          sorted.map((a, i) => {\n            const tags = tagsById[a.id] ?? [];\n            return (\n              <Link\n                key={a.id}\n                to={a.to}\n                className=\"flex items-center gap-3 px-6 py-3 hover:bg-muted/30 transition-colors group\"\n              >\n                <div\n                  className=\"h-9 w-9 rounded-full flex items-center justify-center text-xs font-bold text-white shrink-0\"\n                  style={{ backgroundColor: shareColor(i) }}\n                >\n                  {initials(a.label)}\n                </div>\n                <div className=\"min-w-0 flex-1\">\n                  <p className=\"text-sm font-semibold text-foreground truncate\">{a.label}</p>\n                  <div className=\"flex items-center gap-1.5 mt-0.5 flex-wrap\">\n                    <span className={cn(\"w-1.5 h-1.5 rounded-full\", a.connected || a.isManual ? \"bg-chart-green\" : \"bg-muted-foreground\")} />\n                    <span className=\"text-[11px] text-muted-foreground\">\n                      {a.isManual ? \"Manual\" : a.connected ? \"Connected\" : \"Disconnected\"} · {a.nativeCurrency ?? a.currency}\n                    </span>\n                    {tags.map((t) => (\n                      <span key={t.id} className=\"inline-flex items-center gap-1 text-[10px] px-1.5 py-0 rounded-full bg-muted/60 text-muted-foreground\">\n                        <span aria-hidden className=\"h-1.5 w-1.5 rounded-full shrink-0\" style={{ backgroundColor: tagColorValue(t.color) }} />\n                        {t.name}\n                      </span>\n                    ))}\n                  </div>\n                </div>\n                <div className=\"text-right shrink-0\">\n                  <p className=\"text-sm font-bold text-foreground tabular-nums\">{fmt(a.value, a.currency)}</p>\n                  <p className=\"text-[11px] text-muted-foreground tabular-nums\">\n                    {total > 0 ? `${a.pct.toFixed(1)}%` : \"—\"}\n                  </p>\n                </div>\n                {a.actions && <div className=\"shrink-0 opacity-0 group-hover:opacity-100 transition-opacity\">{a.actions}</div>}\n              </Link>\n            );\n          })\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/EditHoldingDialog.tsx",
      "type": "text/plain",
      "content": "import { useState, useEffect } from \"react\";\nimport { format, parseISO } from \"date-fns\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Button } from \"@/components/ui/button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Calendar as CalendarUI } from \"@/components/ui/calendar\";\nimport { Calendar as CalendarIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type { Holding } from \"@/types/finance\";\nimport { mergeHistory, historyPointsFor, ASSET_CATEGORIES } from \"@/lib/holdings\";\n\ninterface Props {\n  holding: Holding;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  onSave: (updated: Holding) => void;\n}\n\nexport function EditHoldingDialog({ holding, open, onOpenChange, onSave }: Props) {\n  const [name, setName] = useState(holding.name);\n  const [category, setCategory] = useState(holding.category ?? \"Other\");\n  const [currency, setCurrency] = useState(holding.currency ?? \"USD\");\n  const [details, setDetails] = useState(holding.details ?? \"\");\n  const [amount, setAmount] = useState(String(holding.costBasis));\n  const [currentValue, setCurrentValue] = useState(String(holding.price));\n  const [ticker, setTicker] = useState(holding.symbol ?? \"\");\n  const [quantity, setQuantity] = useState(String(holding.shares ?? 1));\n  const [dateObj, setDateObj] = useState<Date>(() => {\n    if (holding.dateInvested) {\n      try { return parseISO(holding.dateInvested); } catch { /* fall through */ }\n    }\n    return new Date();\n  });\n\n  // Sync fields whenever the holding prop changes (e.g. opening for a different row)\n  useEffect(() => {\n    setName(holding.name);\n    setCategory(holding.category ?? \"Other\");\n    setCurrency(holding.currency ?? \"USD\");\n    setDetails(holding.details ?? \"\");\n    setAmount(String(holding.costBasis));\n    setCurrentValue(String(holding.price));\n    setTicker(holding.symbol ?? \"\");\n    setQuantity(String(holding.shares ?? 1));\n    if (holding.dateInvested) {\n      try { setDateObj(parseISO(holding.dateInvested)); } catch { setDateObj(new Date()); }\n    } else {\n      setDateObj(new Date());\n    }\n  }, [holding]);\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    if (!name || !amount) return;\n\n    const dateStr = format(dateObj, \"yyyy-MM-dd\");\n    const costBasis = parseFloat(amount) || 0;\n    const price = currentValue ? parseFloat(currentValue) : costBasis;\n    const shares = parseFloat(quantity) > 0 ? parseFloat(quantity) : 1;\n\n    // Merge, never replace: this dialog used to rebuild `history` from scratch,\n    // so editing a holding erased every price point it had accumulated.\n    const history = mergeHistory(\n      holding.history,\n      historyPointsFor(dateStr, costBasis, price, shares)\n    );\n\n    const derived = name.length > 4 ? name.substring(0, 4).toUpperCase() : name.toUpperCase();\n    onSave({\n      ...holding,\n      name,\n      // A linked public ticker wins (enables live tracking); else fall back to a\n      // name-derived placeholder for still-private holdings.\n      symbol: ticker.trim() ? ticker.trim().toUpperCase() : derived,\n      shares,\n      price,\n      costBasis,\n      change: price - costBasis,\n      changePercent: costBasis > 0 ? ((price - costBasis) / costBasis) * 100 : 0,\n      currency,\n      category,\n      details,\n      dateInvested: dateStr,\n      history,\n    });\n    onOpenChange(false);\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"sm:max-w-[425px]\">\n        <DialogHeader>\n          <DialogTitle>Edit Investment</DialogTitle>\n        </DialogHeader>\n        <form onSubmit={handleSubmit} className=\"space-y-4 pt-4\">\n          <div className=\"grid grid-cols-2 gap-4\">\n            <div className=\"space-y-2\">\n              <Label>Asset Name</Label>\n              <Input\n                placeholder=\"e.g. SpaceX Series A\"\n                value={name}\n                onChange={(e) => setName(e.target.value)}\n                required\n              />\n            </div>\n            <div className=\"space-y-2\">\n              <Label>Category</Label>\n              <Select value={category} onValueChange={setCategory}>\n                <SelectTrigger><SelectValue /></SelectTrigger>\n                <SelectContent>\n                  {ASSET_CATEGORIES.map((c) => (\n                    <SelectItem key={c} value={c}>{c}</SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n            <div className=\"space-y-2 flex flex-col pt-0.5\">\n              <Label className=\"mb-1\">Investment Date</Label>\n              <Popover>\n                <PopoverTrigger asChild>\n                  <Button\n                    variant=\"outline\"\n                    className={cn(\"w-full justify-start text-left font-normal\", !dateObj && \"text-muted-foreground\")}\n                  >\n                    <CalendarIcon className=\"mr-2 h-4 w-4\" />\n                    {dateObj ? format(dateObj, \"PPP\") : \"Pick a date\"}\n                  </Button>\n                </PopoverTrigger>\n                <PopoverContent className=\"w-auto p-0 z-[100]\" align=\"start\">\n                  <CalendarUI mode=\"single\" selected={dateObj} onSelect={(d) => d && setDateObj(d)} initialFocus />\n                </PopoverContent>\n              </Popover>\n            </div>\n            <div className=\"space-y-2\">\n              <Label>Currency</Label>\n              <Select value={currency} onValueChange={setCurrency}>\n                <SelectTrigger><SelectValue /></SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"USD\">USD</SelectItem>\n                  <SelectItem value=\"INR\">INR</SelectItem>\n                  <SelectItem value=\"EUR\">EUR</SelectItem>\n                  <SelectItem value=\"GBP\">GBP</SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n            <div className=\"space-y-2\">\n              <Label>Avg cost / share</Label>\n              <Input\n                type=\"number\"\n                step=\"0.01\"\n                placeholder=\"Cost per share\"\n                value={amount}\n                onChange={(e) => setAmount(e.target.value)}\n                required\n              />\n            </div>\n            <div className=\"space-y-2\">\n              <Label>Quantity / Shares</Label>\n              <Input\n                type=\"number\"\n                step=\"0.0001\"\n                min=\"0.0001\"\n                placeholder=\"1\"\n                value={quantity}\n                onChange={(e) => setQuantity(e.target.value)}\n              />\n            </div>\n            <div className=\"space-y-2\">\n              <Label>Current Value</Label>\n              <Input\n                type=\"number\"\n                step=\"0.01\"\n                placeholder=\"Leave empty if unchanged\"\n                value={currentValue}\n                onChange={(e) => setCurrentValue(e.target.value)}\n              />\n            </div>\n            <div className=\"space-y-2 col-span-2\">\n              <Label>Public ticker (optional)</Label>\n              <Input\n                placeholder=\"e.g. AAPL, RELIANCE, set once it's public for live tracking\"\n                value={ticker}\n                onChange={(e) => setTicker(e.target.value)}\n              />\n              <p className=\"text-xs text-muted-foreground\">\n                For a public ticker, enter avg cost in its trading currency (e.g. USD for SPCX). The live price will display in that native currency.\n              </p>\n            </div>\n            <div className=\"space-y-2 col-span-2\">\n              <Label>Additional Details / Notes</Label>\n              <Input\n                placeholder=\"e.g. Purchased through Securitize\"\n                value={details}\n                onChange={(e) => setDetails(e.target.value)}\n              />\n            </div>\n          </div>\n          <Button type=\"submit\" className=\"w-full\">Save Changes</Button>\n        </form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/ExposurePanel.tsx",
      "type": "text/plain",
      "content": "import { Card, CardContent, CardHeader, CardTitle, CardDescription } from \"@/components/ui/card\";\nimport { exposureStats } from \"@/lib/holdings\";\nimport type { Holding } from \"@/types/finance\";\n\nfunction Bar({ label, value, pct }: { label: string; value: string; pct: number }) {\n  return (\n    <div className=\"space-y-1.5\">\n      <div className=\"flex items-baseline justify-between text-sm\">\n        <span className=\"text-foreground font-medium\">{label}</span>\n        <span className=\"text-muted-foreground tabular-nums\">{value}</span>\n      </div>\n      <div className=\"h-1.5 w-full rounded-full bg-muted overflow-hidden\">\n        <div className=\"h-full rounded-full bg-primary\" style={{ width: `${Math.min(100, Math.max(0, pct))}%` }} />\n      </div>\n    </div>\n  );\n}\n\n/**\n * How concentrated the portfolio is, not a recommendation, a read: the\n * single biggest position, the top three combined, and whichever currency\n * dominates. Same underlying holdings the Allocation panel groups, a\n * different question about them.\n */\nexport function ExposurePanel({ holdings }: { holdings: Holding[] }) {\n  const stats = exposureStats(holdings);\n\n  return (\n    <Card className=\"border-border/50\">\n      <CardHeader className=\"pb-3\">\n        <CardTitle className=\"text-base font-semibold\">Exposure</CardTitle>\n        <CardDescription>Concentration across the portfolio</CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-4\">\n        {stats.total === 0 ? (\n          <p className=\"text-sm text-muted-foreground text-center py-4\">No holdings yet.</p>\n        ) : (\n          <>\n            {stats.topHolding && (\n              <Bar label={`Top holding · ${stats.topHolding.symbol}`} value={`${stats.topHolding.pct.toFixed(1)}%`} pct={stats.topHolding.pct} />\n            )}\n            <Bar label=\"Top 3 holdings\" value={`${stats.top3Pct.toFixed(1)}%`} pct={stats.top3Pct} />\n            {stats.topCurrency && (\n              <Bar label={`${stats.topCurrency.currency} exposure`} value={`${stats.topCurrency.pct.toFixed(1)}%`} pct={stats.topCurrency.pct} />\n            )}\n          </>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/GoalsPanel.tsx",
      "type": "text/plain",
      "content": "import { Link } from \"react-router-dom\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Card, CardContent, CardHeader, CardTitle, CardDescription } from \"@/components/ui/card\";\nimport { api } from \"@/lib/api\";\nimport { Flag } from \"lucide-react\";\n\ninterface GoalDto {\n  id: string;\n  name: string;\n  targetAmount: number;\n  currentAmount: number;\n  color: string;\n}\n\nconst MAX_SHOWN = 3;\n\n/**\n * Surfaces the user's existing goals (Goals Tracker) here too, so checking\n * progress doesn't mean leaving the investments view. This is not a \"funded\n * by investments\" computation, goals aren't linked to specific holdings or\n * accounts anywhere in this app, just each goal's own tracked progress.\n */\nexport function GoalsPanel() {\n  const { data: goals, isLoading } = useQuery({\n    queryKey: [\"goals\"],\n    queryFn: () => api.get<GoalDto[]>(\"goals\"),\n  });\n\n  const shown = (goals ?? []).slice(0, MAX_SHOWN);\n\n  return (\n    <Card className=\"border-border/50\">\n      <CardHeader className=\"pb-3\">\n        <div className=\"flex items-start justify-between gap-3\">\n          <div>\n            <CardTitle className=\"text-base font-semibold\">Goals</CardTitle>\n            <CardDescription>Your active savings goals</CardDescription>\n          </div>\n          <Link to=\"/goals\" className=\"text-xs text-primary hover:text-primary/80 font-medium shrink-0\">\n            View all\n          </Link>\n        </div>\n      </CardHeader>\n      <CardContent className=\"space-y-4\">\n        {isLoading ? (\n          <p className=\"text-sm text-muted-foreground text-center py-4\">Loading…</p>\n        ) : shown.length === 0 ? (\n          <div className=\"text-center py-4 space-y-2\">\n            <Flag size={20} strokeWidth={1.5} className=\"text-muted-foreground/40 mx-auto\" />\n            <Link to=\"/goals\" className=\"text-sm text-primary hover:text-primary/80 font-medium\">\n              Create your first goal\n            </Link>\n          </div>\n        ) : (\n          shown.map((g) => {\n            const pct = g.targetAmount > 0 ? Math.min(100, (g.currentAmount / g.targetAmount) * 100) : 0;\n            return (\n              <div key={g.id} className=\"space-y-1.5\">\n                <div className=\"flex items-baseline justify-between text-sm\">\n                  <span className=\"text-foreground font-medium truncate\">{g.name}</span>\n                  <span className=\"text-muted-foreground tabular-nums shrink-0 ml-2\">{pct.toFixed(0)}%</span>\n                </div>\n                <div className=\"h-1.5 w-full rounded-full bg-muted overflow-hidden\">\n                  <div className=\"h-full rounded-full\" style={{ width: `${pct}%`, backgroundColor: g.color || \"hsl(var(--primary))\" }} />\n                </div>\n              </div>\n            );\n          })\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/HoldingChart.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useMemo, useState } from \"react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { TimeSeriesChart } from \"@/components/charts/TimeSeriesChart\";\nimport { SegmentedControl } from \"@/components/charts/SegmentedControl\";\nimport { Loader2, TrendingUp, TrendingDown, Info } from \"lucide-react\";\nimport { api } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\nimport type { Holding } from \"@/types/finance\";\nimport { usePrivacy } from \"@/lib/privacy\";\nimport { formatMoney } from \"@/lib/format\";\n\nconst PERIODS = [\"1mo\", \"3mo\", \"6mo\", \"1y\", \"2y\", \"5y\", \"10y\"] as const;\nconst PERIOD_LABEL: Record<string, string> = { \"1mo\": \"1M\", \"3mo\": \"3M\", \"6mo\": \"6M\", \"1y\": \"1Y\", \"2y\": \"2Y\", \"5y\": \"5Y\", \"10y\": \"10Y\" };\nconst PERIOD_OPTIONS = PERIODS.map((p) => ({ value: p as string, label: PERIOD_LABEL[p] }));\n\ntype Quote = Awaited<ReturnType<typeof api.market.history>>;\n\nfunction fmtPrice(n: number, currency: string) {\n  return formatMoney(n, currency, { decimals: 2 });\n}\n\ninterface Props {\n  holding: Holding | null;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\n/**\n * Live price + history for a manual holding linked to a public ticker (the\n * SpaceX-went-public case). Resolves the holding's symbol via the backend\n * /market/history endpoint (yfinance). Shows guidance when the symbol has no\n * public market data (still private / wrong ticker).\n */\nexport function HoldingChart({ holding, open, onOpenChange }: Props) {\n  // Local formatter reads privacy mode; subscribe so toggling it re-renders.\n  usePrivacy();\n  const [period, setPeriod] = useState<string>(\"1y\");\n  const [data, setData] = useState<Quote | null>(null);\n  const [loading, setLoading] = useState(false);\n\n  const symbol = holding?.symbol ?? \"\";\n  const currency = holding?.currency ?? \"USD\";\n\n  useEffect(() => {\n    if (!open || !symbol) return;\n    let cancelled = false;\n    setLoading(true);\n    setData(null);\n    api.market\n      .history(symbol, currency, period)\n      .then((res) => { if (!cancelled) setData(res); })\n      .catch(() => { if (!cancelled) setData(null); })\n      .finally(() => { if (!cancelled) setLoading(false); });\n    return () => { cancelled = true; };\n  }, [open, symbol, currency, period]);\n\n  const up = (data?.changePercent ?? 0) >= 0;\n  const series = useMemo(\n    () => (data?.series ?? []).map((p) => ({ date: String(p.date).slice(0, 10), value: p.close })),\n    [data]\n  );\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"sm:max-w-[560px]\">\n        <DialogHeader>\n          <DialogTitle className=\"flex items-center gap-2\">\n            {holding?.name}\n            {data?.found && data.resolved && (\n              <span className=\"text-xs font-normal text-muted-foreground\">({data.resolved})</span>\n            )}\n          </DialogTitle>\n        </DialogHeader>\n\n        {loading ? (\n          <div className=\"flex flex-col items-center justify-center py-16 gap-3 text-muted-foreground\">\n            <Loader2 className=\"h-8 w-8 animate-spin\" />\n            <p className=\"text-sm\">Fetching live market data…</p>\n          </div>\n        ) : data?.found && data.series.length > 1 ? (\n          <div className=\"space-y-4\">\n            <div className=\"flex items-end justify-between\">\n              <div>\n                <p className=\"text-3xl font-bold tabular-nums text-foreground\">{fmtPrice(data.price ?? 0, data.currency)}</p>\n                <p className={cn(\"text-sm font-medium tabular-nums flex items-center gap-1\", up ? \"text-chart-green\" : \"text-chart-red\")}>\n                  {up ? <TrendingUp size={14} /> : <TrendingDown size={14} />}\n                  {up ? \"+\" : \"\"}{fmtPrice(data.change ?? 0, data.currency)} ({up ? \"+\" : \"\"}{data.changePercent?.toFixed(2)}%)\n                </p>\n              </div>\n              <SegmentedControl label=\"Price history period\" options={PERIOD_OPTIONS} value={period} onChange={setPeriod} className=\"[&>button]:px-2\" />\n            </div>\n\n            <TimeSeriesChart\n              data={series}\n              height={240}\n              label=\"Price\"\n              formatValue={(v) => fmtPrice(v, data.currency)}\n              formatAxisValue={(v) => fmtPrice(v, data.currency)}\n              yAxisWidth={60}\n            />\n            <p className=\"text-[11px] text-muted-foreground text-center\">Live market data via Yahoo Finance. Tracking <span className=\"font-medium text-foreground\">{symbol}</span>.</p>\n          </div>\n        ) : (\n          <div className=\"flex flex-col items-center justify-center py-12 gap-3 text-center\">\n            <div className=\"h-12 w-12 rounded-full bg-muted flex items-center justify-center\">\n              <Info size={22} strokeWidth={1.5} className=\"text-muted-foreground\" />\n            </div>\n            <div className=\"space-y-1 max-w-sm\">\n              <p className=\"text-sm font-semibold text-foreground\">No public market data for “{symbol}”</p>\n              <p className=\"text-xs text-muted-foreground\">\n                This looks like a private or unlisted holding. Once it goes public, edit the holding and set its\n                <span className=\"font-medium text-foreground\"> public ticker</span> (e.g. AAPL, RELIANCE) to track the live price here.\n              </p>\n            </div>\n          </div>\n        )}\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/PortfolioAllocation.tsx",
      "type": "text/plain",
      "content": "import { useMemo, useState } from \"react\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\";\nimport { WeightRing } from \"@/components/charts/WeightRing\";\nimport { Input } from \"@/components/ui/input\";\nimport { Badge } from \"@/components/ui/badge\";\nimport type { Holding } from \"@/types/finance\";\nimport { cn } from \"@/lib/utils\";\nimport { usePrivacy } from \"@/lib/privacy\";\nimport { formatMoney } from \"@/lib/format\";\nimport { Search } from \"lucide-react\";\n\ninterface Props {\n  holdings: Holding[];\n  currency: string;\n  investmentAccounts: { id: string; label: string }[];\n}\n\nfunction fmt(v: number, c: string) {\n  return formatMoney(v, c, { decimals: 0 });\n}\n\nconst TOP_N = 8;\n\n/**\n * Consolidated holdings, ranked by weight - the same table + ring pattern\n * already used on the per-account holdings page, just rolled up across every\n * connected source. Replaces the portfolio heatmap: a treemap needs every\n * tile to size against the others, so a handful of holdings with a missing\n * or zero price (common for a freshly-added manual position, or a brokerage\n * quote that hasn't synced yet) rendered as an invisible zero-area tile with\n * nothing on screen to say why. A table row always shows its value, even\n * when that value is $0.\n */\nexport function PortfolioAllocation({ holdings, currency, investmentAccounts }: Props) {\n  // Local formatter reads privacy mode; subscribe so toggling it re-renders.\n  usePrivacy();\n  const [query, setQuery] = useState(\"\");\n\n  const accountLabel = useMemo(\n    () => new Map(investmentAccounts.map((a) => [a.id, a.label])),\n    [investmentAccounts]\n  );\n\n  const { rows, top, restCount, restValue, restWeight } = useMemo(() => {\n    const inCur = holdings.filter((h) => (h.currency ?? currency) === currency);\n    const total = inCur.reduce((s, h) => s + h.shares * h.price, 0);\n    const rows = inCur\n      .map((h) => {\n        const value = h.shares * h.price;\n        return {\n          ...h,\n          value,\n          returnVal: h.change * h.shares,\n          weight: total > 0 ? (value / total) * 100 : 0,\n          accountName: (h.investmentAccountId && accountLabel.get(h.investmentAccountId)) || \"Manual\",\n        };\n      })\n      .sort((a, b) => b.value - a.value);\n\n    const top = rows.slice(0, TOP_N);\n    const rest = rows.slice(TOP_N);\n    const restValue = rest.reduce((s, h) => s + h.value, 0);\n    return { rows, top, restCount: rest.length, restValue, restWeight: total > 0 ? (restValue / total) * 100 : 0 };\n  }, [holdings, currency, accountLabel]);\n\n  const filtered = useMemo(() => {\n    const q = query.trim().toLowerCase();\n    if (!q) return null;\n    return rows.filter((h) => h.symbol.toLowerCase().includes(q) || h.name.toLowerCase().includes(q));\n  }, [rows, query]);\n\n  const shown = filtered ?? top;\n\n  if (rows.length === 0) return null;\n\n  return (\n    <div className=\"space-y-3\">\n      <div className=\"relative max-w-xs\">\n        <Search size={14} strokeWidth={1.5} className=\"absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground\" />\n        <Input\n          value={query}\n          onChange={(e) => setQuery(e.target.value)}\n          placeholder=\"Filter holdings…\"\n          className=\"pl-8 h-8 text-sm bg-background\"\n        />\n      </div>\n      <div className=\"overflow-x-auto\">\n        <Table>\n          <TableHeader>\n            <TableRow className=\"border-border/30 hover:bg-transparent\">\n              <TableHead className=\"text-muted-foreground text-[10px] font-semibold uppercase tracking-wider h-9\">Holding</TableHead>\n              <TableHead className=\"text-muted-foreground text-[10px] font-semibold uppercase tracking-wider h-9\">Account</TableHead>\n              <TableHead className=\"text-muted-foreground text-[10px] font-semibold uppercase tracking-wider h-9\">Type</TableHead>\n              <TableHead className=\"text-muted-foreground text-[10px] font-semibold uppercase tracking-wider h-9 text-right w-24\">Weight</TableHead>\n              <TableHead className=\"text-muted-foreground text-[10px] font-semibold uppercase tracking-wider h-9 text-right\">Value</TableHead>\n              <TableHead className=\"text-muted-foreground text-[10px] font-semibold uppercase tracking-wider h-9 text-right pr-1\">Return</TableHead>\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {shown.length === 0 ? (\n              <TableRow className=\"border-border/20 hover:bg-transparent\">\n                <TableCell colSpan={6} className=\"py-8 text-center text-sm text-muted-foreground\">\n                  No holdings match \"{query}\".\n                </TableCell>\n              </TableRow>\n            ) : (\n              shown.map((h, i) => {\n                const returnPct = Number.isFinite(h.changePercent) ? h.changePercent : null;\n                const isPositive = h.changePercent >= 0;\n                return (\n                  <TableRow\n                    key={h.id}\n                    className=\"border-border/20 transition-colors hover:bg-muted/20 animate-fade-in\"\n                    style={{ animationDelay: `${i * 30}ms` }}\n                  >\n                    <TableCell className=\"py-2.5\">\n                      <p className=\"text-sm font-bold text-foreground\">{h.symbol}</p>\n                      <p className=\"text-xs text-muted-foreground truncate max-w-[180px]\">{h.name}</p>\n                    </TableCell>\n                    <TableCell className=\"py-2.5\">\n                      <Badge variant=\"outline\" className=\"font-normal text-[11px]\">{h.accountName}</Badge>\n                    </TableCell>\n                    <TableCell className=\"py-2.5 text-xs text-muted-foreground\">{h.category || \"—\"}</TableCell>\n                    <TableCell className=\"text-right py-2.5\">\n                      <div className=\"flex items-center justify-end gap-1.5\">\n                        <WeightRing pct={h.weight} />\n                        <span className=\"text-xs text-muted-foreground tabular-nums w-9 text-right\">{h.weight.toFixed(1)}%</span>\n                      </div>\n                    </TableCell>\n                    <TableCell className=\"text-sm text-foreground text-right py-2.5 font-bold tabular-nums\">{fmt(h.value, h.currency ?? currency)}</TableCell>\n                    <TableCell className=\"text-right py-2.5 pr-1\">\n                      <span className={cn(\"text-xs font-bold tabular-nums\", isPositive ? \"text-chart-green\" : \"text-chart-red\")}>\n                        {returnPct === null ? \"—\" : `${isPositive ? \"+\" : \"\"}${returnPct.toFixed(2)}%`}\n                      </span>\n                    </TableCell>\n                  </TableRow>\n                );\n              })\n            )}\n            {!filtered && restCount > 0 && (\n              <TableRow className=\"border-border/20\">\n                <TableCell colSpan={3} className=\"py-2.5 text-sm text-muted-foreground\">{restCount} other holding{restCount === 1 ? \"\" : \"s\"}</TableCell>\n                <TableCell className=\"text-right py-2.5\">\n                  <div className=\"flex items-center justify-end gap-1.5\">\n                    <WeightRing pct={restWeight} />\n                    <span className=\"text-xs text-muted-foreground tabular-nums w-9 text-right\">{restWeight.toFixed(1)}%</span>\n                  </div>\n                </TableCell>\n                <TableCell className=\"text-sm text-muted-foreground text-right py-2.5 tabular-nums\">{fmt(restValue, currency)}</TableCell>\n                <TableCell className=\"text-right py-2.5 pr-1\" />\n              </TableRow>\n            )}\n          </TableBody>\n        </Table>\n      </div>\n      {!filtered && restCount > 0 && (\n        <p className=\"text-[11px] text-muted-foreground/70 px-1\">\n          Showing {top.length} of {rows.length} holdings.\n        </p>\n      )}\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/PortfolioValueChart.tsx",
      "type": "text/plain",
      "content": "import { useMemo, useState } from \"react\";\nimport { Card, CardContent, CardHeader } from \"@/components/ui/card\";\nimport { TimeSeriesChart } from \"@/components/charts/TimeSeriesChart\";\nimport { SegmentedControl } from \"@/components/charts/SegmentedControl\";\nimport { ChartTypeToggle } from \"@/components/charts/ChartTypeToggle\";\nimport { portfolioValueHistory, mergeSnapshotsIntoHistory } from \"@/lib/holdings\";\nimport { periodChange } from \"@/lib/balanceSheet\";\nimport { toDateString, formatCompactMoney } from \"@/lib/format\";\nimport { usePrivacy } from \"@/lib/privacy\";\nimport { formatMoney } from \"@/lib/format\";\nimport type { Holding } from \"@/types/finance\";\nimport { TrendingDown, TrendingUp, LineChart as LineChartIcon, BarChart3 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst PERIODS = [\n  { label: \"1M\", days: 30 },\n  { label: \"3M\", days: 90 },\n  { label: \"1Y\", days: 365 },\n  { label: \"All\", days: 999 },\n];\nconst PERIOD_OPTIONS = PERIODS.map((p) => ({ value: p.label, label: p.label }));\nconst CHART_TYPES = [\n  { value: \"line\", icon: LineChartIcon, label: \"Line\" },\n  { value: \"bar\", icon: BarChart3, label: \"Bar\" },\n] as const;\n\nfunction fmt(v: number, c: string) {\n  return formatMoney(v, c, { decimals: 2 });\n}\n\n/**\n * \"Total portfolio value\" hero: the combined value of every same-currency\n * holding over time, see portfolioValueHistory for how sparse per-holding\n * history is turned into one series. A portfolio that's mostly brokerage-\n * synced (no stored history) will show a flatter early curve than reality,\n * an honest gap given the data actually available rather than a fabricated\n * smooth line - though as `snapshots` (a real recorded total per day,\n * see usePortfolioSnapshotOnMount) accumulate, they take over for any date\n * they cover, since they're the actual figure rather than an approximation.\n */\nexport function PortfolioValueChart({\n  holdings, currency, snapshots,\n}: {\n  holdings: Holding[];\n  currency: string;\n  snapshots?: { date: string; value: number }[];\n}) {\n  usePrivacy();\n  const [period, setPeriod] = useState(\"1Y\");\n  const [chartType, setChartType] = useState<\"line\" | \"bar\">(\"line\");\n\n  const allData = useMemo(\n    () => mergeSnapshotsIntoHistory(portfolioValueHistory(holdings, currency), snapshots ?? []),\n    [holdings, currency, snapshots]\n  );\n\n  const data = useMemo(() => {\n    const days = PERIODS.find((p) => p.label === period)?.days;\n    if (!days || days >= 999) return allData;\n    const cutoff = new Date();\n    cutoff.setDate(cutoff.getDate() - days);\n    const cutoffStr = toDateString(cutoff);\n    return allData.filter((d) => d.date >= cutoffStr);\n  }, [allData, period]);\n\n  const change = useMemo(() => periodChange(data.map((d) => d.value)), [data]);\n  const latest = data[data.length - 1]?.value ?? 0;\n\n  return (\n    <Card className=\"border-border/50\">\n      <CardHeader className=\"pb-2\">\n        <p className=\"text-xs uppercase tracking-wider font-semibold text-muted-foreground/60\">Total portfolio value</p>\n        <p className=\"text-3xl font-bold text-foreground tabular-nums tracking-tight\">{fmt(latest, currency)}</p>\n        {change && (\n          <p className=\"flex items-center gap-1 text-sm tabular-nums\">\n            {change.amount > 0 ? (\n              <TrendingUp size={14} strokeWidth={1.75} className=\"text-chart-green\" aria-hidden />\n            ) : change.amount < 0 ? (\n              <TrendingDown size={14} strokeWidth={1.75} className=\"text-chart-red\" aria-hidden />\n            ) : null}\n            <span className={cn(\"font-medium\", change.amount > 0 ? \"text-chart-green\" : change.amount < 0 ? \"text-chart-red\" : \"text-foreground\")}>\n              {change.amount > 0 ? \"+\" : change.amount < 0 ? \"-\" : \"\"}{fmt(Math.abs(change.amount), currency)}\n              {change.pct !== null && ` (${change.pct > 0 ? \"+\" : \"\"}${change.pct.toFixed(1)}%)`}\n            </span>\n            <span className=\"text-muted-foreground\">over {period === \"All\" ? \"all time\" : period}</span>\n          </p>\n        )}\n        <div className=\"flex items-center justify-end gap-2 pt-1\">\n          <ChartTypeToggle options={CHART_TYPES} value={chartType} onChange={setChartType} label=\"Chart type\" />\n          <SegmentedControl label=\"Chart period\" options={PERIOD_OPTIONS} value={period} onChange={setPeriod} />\n        </div>\n      </CardHeader>\n      <CardContent>\n        {data.length === 0 ? (\n          <p className=\"text-sm text-muted-foreground text-center py-16\">No value history yet.</p>\n        ) : (\n          <TimeSeriesChart\n            data={data}\n            height={260}\n            label=\"Portfolio value\"\n            formatValue={(v) => fmt(v, currency)}\n            formatAxisValue={(v) => formatCompactMoney(v, currency)}\n            chartType={chartType}\n          />\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/SourceCard.tsx",
      "type": "text/plain",
      "content": "import { Link } from \"react-router-dom\";\nimport { ArrowRight, TrendingUp, TrendingDown } from \"lucide-react\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { cn } from \"@/lib/utils\";\nimport { usePrivacy } from \"@/lib/privacy\";\nimport { formatMoney } from \"@/lib/format\";\n\ninterface CurrencyBreakdown {\n  value: number;\n  gain: number;\n  gainPct: number;\n}\n\ninterface Props {\n  name: string;\n  icon: React.ReactNode;\n  to: string;\n  portfolioValue: number;\n  totalGain: number;\n  totalGainPct: number;\n  holdingsCount: number;\n  currency: string;\n  connected: boolean;\n  /** Optional action buttons rendered in the card header (e.g. rename/delete for manual accounts). */\n  actions?: React.ReactNode;\n  /**\n   * When an account holds multiple currencies, pass a per-currency breakdown\n   * so we avoid displaying a cross-currency sum. When present and length > 1,\n   * the single-figure display is replaced with a stacked per-currency view.\n   */\n  currencyBreakdown?: Record<string, CurrencyBreakdown>;\n}\n\nfunction fmt(v: number, c: string) {\n  return formatMoney(v, c, { decimals: 2 });\n}\n\nexport function SourceCard({ name, icon, to, portfolioValue, totalGain, totalGainPct, holdingsCount, currency, connected, actions, currencyBreakdown }: Props) {\n  // Local formatter reads privacy mode; subscribe so toggling it re-renders.\n  usePrivacy();\n  const isPositive = totalGain >= 0;\n  const breakdownEntries = currencyBreakdown ? Object.entries(currencyBreakdown) : [];\n  const isMixed = breakdownEntries.length > 1;\n\n  return (\n    <Link to={to} className=\"block group\">\n      <Card className=\"bg-card/60 border-border/50 hover:border-primary/30 transition-all duration-300 hover:shadow-lg hover:shadow-primary/5 overflow-hidden relative\">\n        <div className={cn(\n          \"absolute inset-x-0 bottom-0 h-0.5 transition-all duration-300 opacity-0 group-hover:opacity-100\",\n          isPositive ? \"bg-gradient-to-r from-chart-green/50 to-chart-green\" : \"bg-gradient-to-r from-chart-red/50 to-chart-red\"\n        )} />\n        <CardContent className=\"p-5\">\n          <div className=\"flex items-start justify-between mb-4\">\n            <div className=\"flex items-center gap-3\">\n              <div className=\"w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center shrink-0\">\n                {icon}\n              </div>\n              <div>\n                <h3 className=\"text-sm font-bold text-foreground\">{name}</h3>\n                <div className=\"flex items-center gap-1.5 mt-0.5\">\n                  <span className={cn(\"w-1.5 h-1.5 rounded-full\", connected ? \"bg-chart-green\" : \"bg-muted-foreground\")} />\n                  <span className=\"text-[10px] text-muted-foreground uppercase tracking-wider\">\n                    {connected ? \"Connected\" : \"Disconnected\"}\n                  </span>\n                </div>\n              </div>\n            </div>\n            <div className=\"flex items-center gap-1 mt-0.5\">\n              {actions}\n              <ArrowRight size={16} className=\"text-muted-foreground/40 group-hover:text-primary group-hover:translate-x-0.5 transition-all duration-200\" />\n            </div>\n          </div>\n\n          <div className=\"space-y-3\">\n            {isMixed ? (\n              <>\n                <div className=\"space-y-0.5\">\n                  {breakdownEntries.map(([c, bd]) => (\n                    <p key={c} className=\"text-lg font-bold text-foreground tabular-nums tracking-tight leading-tight\">\n                      {fmt(bd.value, c)}\n                    </p>\n                  ))}\n                </div>\n                <div className=\"flex items-center justify-between\">\n                  <div className=\"space-y-0.5\">\n                    {breakdownEntries.map(([c, bd]) => (\n                      <div key={c} className=\"flex items-center gap-1\">\n                        {bd.gain >= 0 ? <TrendingUp size={11} className=\"text-chart-green\" /> : <TrendingDown size={11} className=\"text-chart-red\" />}\n                        <span className={cn(\"text-xs font-bold tabular-nums\", bd.gain >= 0 ? \"text-chart-green\" : \"text-chart-red\")}>\n                          {bd.gain >= 0 ? \"+\" : \"\"}{fmt(bd.gain, c)} ({bd.gainPct >= 0 ? \"+\" : \"\"}{bd.gainPct.toFixed(2)}%)\n                        </span>\n                      </div>\n                    ))}\n                  </div>\n                  <span className=\"text-xs text-muted-foreground self-end\">{holdingsCount} holdings</span>\n                </div>\n              </>\n            ) : (\n              <>\n                <div>\n                  <p className=\"text-2xl font-bold text-foreground tabular-nums tracking-tight\">\n                    {fmt(portfolioValue, currency)}\n                  </p>\n                </div>\n                <div className=\"flex items-center justify-between\">\n                  <div className=\"flex items-center gap-1.5\">\n                    {isPositive ? <TrendingUp size={13} className=\"text-chart-green\" /> : <TrendingDown size={13} className=\"text-chart-red\" />}\n                    <span className={cn(\"text-xs font-bold tabular-nums\", isPositive ? \"text-chart-green\" : \"text-chart-red\")}>\n                      {isPositive ? \"+\" : \"\"}{fmt(totalGain, currency)} ({isPositive ? \"+\" : \"\"}{totalGainPct.toFixed(2)}%)\n                    </span>\n                  </div>\n                  <span className=\"text-xs text-muted-foreground\">{holdingsCount} holdings</span>\n                </div>\n              </>\n            )}\n          </div>\n        </CardContent>\n      </Card>\n    </Link>\n  );\n}\n"
    },
    {
      "name": "src/components/investments/TopMovers.tsx",
      "type": "text/plain",
      "content": "import { useMemo } from \"react\";\nimport { TrendingUp, TrendingDown } from \"lucide-react\";\nimport type { Holding } from \"@/types/finance\";\nimport { cn } from \"@/lib/utils\";\nimport { usePrivacy } from \"@/lib/privacy\";\nimport { formatMoney } from \"@/lib/format\";\n\ninterface Props {\n  holdings: Holding[];\n  currency: string;\n}\n\nfunction fmt(v: number, c: string) {\n  return formatMoney(v, c, { decimals: 2 });\n}\n\nexport function TopMovers({ holdings, currency }: Props) {\n  // Local formatter reads privacy mode; subscribe so toggling it re-renders.\n  usePrivacy();\n  // \"Movers\" means today's movement. Ranking by `changePercent` (total return\n  // since purchase) just listed the oldest winners every day.\n  const move = (h: Holding) => h.dayChangePercent ?? 0;\n\n  const { gainers, losers } = useMemo(() => {\n    const sorted = [...holdings].sort((a, b) => move(b) - move(a));\n    return {\n      gainers: sorted.filter((h) => move(h) > 0).slice(0, 3),\n      losers: sorted.filter((h) => move(h) < 0).slice(-3).reverse(),\n    };\n  }, [holdings]);\n\n  const renderList = (items: Holding[], isGainer: boolean) => (\n    <div className=\"space-y-1\">\n      {items.length === 0 && (\n        <p className=\"text-xs text-muted-foreground py-3 text-center\">No {isGainer ? \"gainers\" : \"losers\"}</p>\n      )}\n      {items.map((h, i) => (\n        <div\n          key={h.id}\n          className=\"flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-muted/40 transition-colors duration-150 animate-fade-in\"\n          style={{ animationDelay: `${i * 80}ms` }}\n        >\n          <div className=\"flex items-center gap-2.5 min-w-0\">\n            <div className={cn(\n              \"w-7 h-7 rounded-md flex items-center justify-center text-xs font-bold shrink-0\",\n              isGainer ? \"bg-chart-green/10 text-chart-green\" : \"bg-chart-red/10 text-chart-red\"\n            )}>\n              {isGainer ? <TrendingUp size={14} /> : <TrendingDown size={14} />}\n            </div>\n            <div className=\"min-w-0\">\n              <p className=\"text-sm font-semibold text-foreground truncate\">{h.symbol}</p>\n              <p className=\"text-xs text-muted-foreground tabular-nums\">{fmt(h.price, h.currency ?? currency)}</p>\n            </div>\n          </div>\n          <span className={cn(\n            \"text-sm font-bold tabular-nums\",\n            isGainer ? \"text-chart-green\" : \"text-chart-red\"\n          )}>\n            {move(h) >= 0 ? \"+\" : \"\"}{move(h).toFixed(2)}%\n          </span>\n        </div>\n      ))}\n    </div>\n  );\n\n  return (\n    <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n      <div>\n        <h3 className=\"text-xs uppercase tracking-wider font-semibold text-muted-foreground/60 px-3 mb-2\">Top gainers today</h3>\n        {renderList(gainers, true)}\n      </div>\n      <div>\n        <h3 className=\"text-xs uppercase tracking-wider font-semibold text-muted-foreground/60 px-3 mb-2\">Top losers today</h3>\n        {renderList(losers, false)}\n      </div>\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/kite/KiteReconnectDialog.tsx",
      "type": "text/plain",
      "content": "import { useCallback, useState } from \"react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { api } from \"@/lib/api\";\nimport { ExternalLink, Loader2, LogIn } from \"lucide-react\";\nimport { toast } from \"sonner\";\n\nconst OAUTH_WINDOW_NAME = \"investment_oauth\";\n\nfunction openLoginPopup(loginUrl: string) {\n  const w = 520;\n  const h = 740;\n  const left = Math.max(0, Math.floor((window.screen.width - w) / 2));\n  const top = Math.max(0, Math.floor((window.screen.height - h) / 4));\n  const features = `popup=yes,width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`;\n  return window.open(loginUrl, OAUTH_WINDOW_NAME, features);\n}\n\ninterface KiteReconnectDialogProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  /** OAuth provider, defaults to \"kite\". */\n  provider?: string;\n}\n\nexport function KiteReconnectDialog({ open, onOpenChange, provider = \"kite\" }: KiteReconnectDialogProps) {\n  const [openingLogin, setOpeningLogin] = useState(false);\n  const [openingPopup, setOpeningPopup] = useState(false);\n\n  const fetchLoginUrl = useCallback(async () => {\n    const { login_url } = await api.investments.connectUrl(provider);\n    return login_url;\n  }, [provider]);\n\n  const handleOpenLogin = async () => {\n    setOpeningLogin(true);\n    try {\n      const login_url = await fetchLoginUrl();\n      window.open(login_url, \"_blank\", \"noopener,noreferrer\");\n    } catch (err: unknown) {\n      toast.error(\"Failed to get login URL: \" + (err instanceof Error ? err.message : \"Unknown error\"));\n    } finally {\n      setOpeningLogin(false);\n    }\n  };\n\n  const handleOpenLoginPopup = async () => {\n    setOpeningPopup(true);\n    try {\n      const login_url = await fetchLoginUrl();\n      const win = openLoginPopup(login_url);\n      if (!win) toast.error(\"Popup blocked, try “Open in a new tab” instead.\");\n    } catch (err: unknown) {\n      toast.error(\"Failed to get login URL: \" + (err instanceof Error ? err.message : \"Unknown error\"));\n    } finally {\n      setOpeningPopup(false);\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"sm:max-w-sm\">\n        <DialogHeader>\n          <DialogTitle>Reconnect your broker</DialogTitle>\n          <DialogDescription>\n            Your session has expired. Log in again and your holdings will refresh automatically.\n          </DialogDescription>\n        </DialogHeader>\n\n        <div className=\"flex flex-col gap-2 pt-2\">\n          <Button className=\"w-full gap-2\" size=\"lg\" onClick={handleOpenLoginPopup} disabled={openingPopup}>\n            {openingPopup ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <LogIn className=\"h-4 w-4\" />}\n            Log in\n          </Button>\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            className=\"w-full gap-2 text-muted-foreground hover:text-foreground\"\n            onClick={handleOpenLogin}\n            disabled={openingLogin}\n          >\n            {openingLogin ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <ExternalLink className=\"h-4 w-4\" />}\n            Open in a new tab\n          </Button>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
    },
    {
      "name": "src/components/landing/AboutAndTrust.tsx",
      "type": "text/plain",
      "content": "import { APP_ORIGIN } from \"@/lib/deployment\";\n\nconst link =\n  \"inline-flex min-h-[44px] items-center gap-1.5 text-sm font-medium text-land-fg underline decoration-land-line underline-offset-4 transition hover:text-land-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-land-accent\";\n\n/** Says plainly what KHMYS is and how it handles data, for people and for\n * crawlers that read only the page text. Every claim here is stated on /privacy. */\nexport function AboutAndTrust() {\n  return (\n    <section\n      id=\"about\"\n      aria-labelledby=\"about-heading\"\n      className=\"mx-auto grid max-w-[1280px] grid-cols-1 gap-10 scroll-mt-24 border-t border-land-line px-5 py-16 sm:px-8 sm:py-20 md:grid-cols-[.8fr_1.2fr] md:gap-20 lg:px-10\"\n    >\n      <div data-reveal>\n        <p className=\"mb-4 text-[11px] font-medium uppercase tracking-[0.2em] text-land-accent\">\n          WHAT IT IS\n        </p>\n        <h2\n          id=\"about-heading\"\n          className=\"text-4xl font-medium leading-[1.1] tracking-[-0.05em] sm:text-5xl\"\n        >\n          What is KHMYS?\n        </h2>\n      </div>\n      <div data-reveal data-reveal-delay={120} className=\"space-y-8\">\n        <p className=\"max-w-xl text-base leading-[1.8] text-land-muted\">\n          <strong className=\"font-semibold text-land-fg\">KHMYS</strong> (Know\n          How Much You Spend) is a personal finance web app. It brings your\n          everyday spending, budgets, accounts, investments and goals into one\n          place, so you can see where your money goes and decide what to do\n          next. Connect a bank, import a CSV or PDF statement, or add\n          transactions yourself.\n        </p>\n        <div>\n          <h3 className=\"text-sm font-medium\">How KHMYS handles your data</h3>\n          <ul className=\"mt-4 max-w-xl list-disc space-y-2.5 pl-5 text-sm leading-7 text-land-muted marker:text-land-accent\">\n            <li>You sign in with Google. KHMYS has no passwords of its own.</li>\n            <li>\n              Bank connections go through Plaid. You enter your bank login in\n              Plaid’s window, never in KHMYS, and KHMYS asks Plaid for\n              transactions only.\n            </li>\n            <li>\n              No advertising, no ad cookies, and nothing sold. KHMYS does not\n              move money.\n            </li>\n            <li>\n              KHMYS does not charge anything today. There is no billing or\n              subscription in the app.\n            </li>\n          </ul>\n        </div>\n        <p className=\"flex flex-wrap items-center gap-x-6\">\n          <a href={`${APP_ORIGIN}/privacy`} className={link}>\n            Privacy policy\n          </a>\n          <a href={`${APP_ORIGIN}/security`} className={link}>\n            Security\n          </a>\n          <a href=\"mailto:support@khmys.com\" className={link}>\n            Contact support\n          </a>\n        </p>\n        <p className=\"max-w-2xl text-sm leading-relaxed text-land-muted\">\n          New to tracking money? Start with{\" \"}\n          <a href={`${APP_ORIGIN}/blog/how-to-track-your-spending`} className=\"text-land-fg underline decoration-land-line underline-offset-4 hover:text-land-accent\">\n            how to track your spending\n          </a>{\" \"}\n          or{\" \"}\n          <a href={`${APP_ORIGIN}/blog/how-to-build-a-monthly-budget`} className=\"text-land-fg underline decoration-land-line underline-offset-4 hover:text-land-accent\">\n            how to build a monthly budget\n          </a>\n          , browse all the{\" \"}\n          <a href={`${APP_ORIGIN}/blog`} className=\"text-land-fg underline decoration-land-line underline-offset-4 hover:text-land-accent\">\n            guides\n          </a>\n          , or read{\" \"}\n          <a href={`${APP_ORIGIN}/about`} className=\"text-land-fg underline decoration-land-line underline-offset-4 hover:text-land-accent\">\n            about KHMYS and who built it\n          </a>\n          .\n        </p>\n      </div>\n    </section>\n  );\n}\n"
    },
    {
      "name": "src/components/plaid/BankReconnectButton.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { toast } from \"sonner\";\nimport { Button } from \"@/components/ui/button\";\nimport { api } from \"@/lib/api\";\nimport { PlaidLinkSession } from \"./PlaidLinkSession\";\n\nexport function BankReconnectButton({ itemId, institution, onReconnected }: { itemId: string; institution: string; onReconnected: () => Promise<unknown> }) {\n  const [token, setToken] = useState<string | null>(null);\n  const [busy, setBusy] = useState(false);\n  const start = async () => {\n    setBusy(true);\n    try { setToken((await api.plaid.createLinkToken(itemId)).link_token); }\n    catch { toast.error(\"Couldn't start bank sign-in. Please try again.\"); }\n    finally { setBusy(false); }\n  };\n  const complete = async () => {\n    setToken(null);\n    setBusy(true);\n    // Update mode repairs the existing Item; never exchange a new public token.\n    try { await onReconnected(); toast.success(`${institution} reconnected and synced`); }\n    catch (error) { toast.error(error instanceof Error ? error.message : \"Bank sign-in completed, but sync needs another attempt.\"); }\n    finally { setBusy(false); }\n  };\n  return token\n    ? <PlaidLinkSession token={token} onSuccess={() => { void complete(); }} onClose={() => setToken(null)} />\n    : <Button size=\"sm\" variant=\"outline\" disabled={busy} onClick={() => { void start(); }} aria-label={`Reconnect ${institution}`}>{busy ? \"Connecting…\" : \"Sign in to bank\"}</Button>;\n}\n"
    },
    {
      "name": "src/components/plaid/ConnectBankButton.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useRef, useState } from \"react\";\nimport { Landmark } from \"lucide-react\";\nimport { toast } from \"sonner\";\nimport { Button, type ButtonProps } from \"@/components/ui/button\";\nimport { api } from \"@/lib/api\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { PlaidLinkSession } from \"./PlaidLinkSession\";\n\nexport function ConnectBankButton({ children = \"Connect your bank\", autoStart = false, ...props }: ButtonProps & { autoStart?: boolean }) {\n  const { syncPlaid, refreshData } = useFinanceData();\n  const [token, setToken] = useState<string | null>(null);\n  const [busy, setBusy] = useState(false);\n  const starting = useRef(false);\n  const autoStarted = useRef(false);\n  const start = async () => {\n    if (starting.current) return;\n    starting.current = true;\n    setBusy(true);\n    try { setToken((await api.plaid.createLinkToken()).link_token); }\n    catch { toast.error(\"Couldn't start bank connection. Please try again.\"); }\n    finally { setBusy(false); starting.current = false; }\n  };\n  useEffect(() => {\n    if (!autoStart) autoStarted.current = false;\n    if (autoStart && !autoStarted.current) { autoStarted.current = true; void start(); }\n    // The initial navigation intent should run only once, including StrictMode.\n  }, [autoStart]);\n  const complete = async (publicToken: string) => {\n    setToken(null);\n    setBusy(true);\n    try {\n      await api.plaid.exchangeToken(publicToken);\n      try { await syncPlaid(); toast.success(\"Bank connected and accounts updated\"); }\n      catch (error) { refreshData(); toast.warning(error instanceof Error ? error.message : \"Bank connected. Sync needs another attempt.\"); }\n    } catch { toast.error(\"Couldn't finish connecting the bank. Please try again.\"); }\n    finally { setBusy(false); }\n  };\n  return token ? <PlaidLinkSession token={token} onSuccess={publicToken => { void complete(publicToken); }} onClose={failed => { setToken(null); if (failed) toast.error(\"Bank connection wasn't completed. Please try again.\"); }} />\n    : <Button {...props} type=\"button\" disabled={busy || props.disabled} onClick={() => { void start(); }}><Landmark size={16} strokeWidth={1.5} aria-hidden />{busy ? \"Connecting…\" : children}</Button>;\n}\n"
    },
    {
      "name": "src/components/plaid/PlaidLinkSession.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useRef } from \"react\";\nimport { usePlaidLink } from \"react-plaid-link\";\nimport { Button } from \"@/components/ui/button\";\n\n/** One automatically opened Link session, shared by connect and reconnect. */\nexport function PlaidLinkSession({ token, onSuccess, onClose }: {\n  token: string; onSuccess: (publicToken: string) => void; onClose: (failed?: boolean) => void;\n}) {\n  const opened = useRef(false);\n  const { open, ready, error } = usePlaidLink({\n    token, onSuccess, onExit: error => onClose(Boolean(error)),\n  });\n  useEffect(() => {\n    if (ready && !opened.current) { opened.current = true; open(); }\n  }, [ready, open]);\n  if (error) return <div className=\"space-y-2\"><p role=\"alert\" className=\"text-sm text-destructive\">The secure bank window couldn't load. Refresh the page and try again.</p><Button size=\"sm\" variant=\"outline\" onClick={() => onClose(true)}>Close</Button></div>;\n  return <div className=\"flex flex-wrap items-center gap-2\"><span role=\"status\" className=\"text-xs text-muted-foreground\">{ready ? \"Bank connection window open\" : \"Opening secure bank connection…\"}</span><Button variant=\"ghost\" size=\"sm\" onClick={() => onClose()}>Cancel</Button></div>;\n}\n"
    },
    {
      "name": "src/components/skeletons/LoadingSkeletons.tsx",
      "type": "text/plain",
      "content": "import { Skeleton } from \"@/components/ui/skeleton\";\nimport { Card, CardContent, CardHeader } from \"@/components/ui/card\";\n\n/** Skeleton for stat cards (used on Home, Analytics) */\nexport function StatCardSkeleton() {\n  return (\n    <Card className=\"bg-card border-border/40\">\n      <CardContent className=\"p-5 space-y-3\">\n        <Skeleton className=\"h-3 w-20\" />\n        <Skeleton className=\"h-8 w-28\" />\n        <Skeleton className=\"h-3 w-16\" />\n      </CardContent>\n    </Card>\n  );\n}\n\n/** Skeleton for transaction rows */\nexport function TransactionRowSkeleton({ count = 5 }: { count?: number }) {\n  return (\n    <div className=\"space-y-0\">\n      {Array.from({ length: count }).map((_, i) => (\n        <div key={i} className=\"flex items-center gap-4 px-5 py-3.5 border-b border-border/20\">\n          <Skeleton className=\"h-8 w-8 rounded-full shrink-0\" />\n          <div className=\"flex-1 space-y-2\">\n            <Skeleton className=\"h-4 w-40\" />\n            <Skeleton className=\"h-3 w-24\" />\n          </div>\n          <Skeleton className=\"h-4 w-16\" />\n        </div>\n      ))}\n    </div>\n  );\n}\n\n/** Skeleton for chart areas */\nexport function ChartSkeleton({ height = 200 }: { height?: number }) {\n  return (\n    <Card className=\"bg-card border-border/40\">\n      <CardHeader className=\"pb-2\">\n        <Skeleton className=\"h-4 w-28\" />\n      </CardHeader>\n      <CardContent>\n        <Skeleton className=\"w-full rounded-lg\" style={{ height }} />\n      </CardContent>\n    </Card>\n  );\n}\n\n/** Skeleton for the full home page */\nexport function DashboardSkeleton() {\n  return (\n    <div className=\"space-y-6 animate-pulse\">\n      {/* Header */}\n      <div className=\"space-y-2\">\n        <Skeleton className=\"h-8 w-48\" />\n        <Skeleton className=\"h-4 w-64\" />\n      </div>\n\n      {/* Stat cards row */}\n      <div className=\"grid grid-cols-2 lg:grid-cols-4 gap-4\">\n        {Array.from({ length: 4 }).map((_, i) => (\n          <StatCardSkeleton key={i} />\n        ))}\n      </div>\n\n      {/* Chart placeholder */}\n      <ChartSkeleton height={250} />\n\n      {/* Transaction list */}\n      <Card className=\"bg-card border-border/40\">\n        <CardHeader className=\"pb-2\">\n          <Skeleton className=\"h-4 w-32\" />\n        </CardHeader>\n        <CardContent className=\"p-0\">\n          <TransactionRowSkeleton count={5} />\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n\n/** Skeleton for full-page content */\nexport function PageSkeleton() {\n  return (\n    <div className=\"space-y-6 animate-pulse\">\n      <div className=\"space-y-2\">\n        <Skeleton className=\"h-7 w-40\" />\n        <Skeleton className=\"h-4 w-56\" />\n      </div>\n      <div className=\"grid grid-cols-2 lg:grid-cols-4 gap-4\">\n        {Array.from({ length: 4 }).map((_, i) => (\n          <StatCardSkeleton key={i} />\n        ))}\n      </div>\n      <Card className=\"bg-card border-border/40\">\n        <CardContent className=\"p-0\">\n          <TransactionRowSkeleton count={8} />\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n"
    },
    {
      "name": "src/components/todos/TodoDetailSheet.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useState } from \"react\";\nimport { Sheet, SheetContent, SheetHeader, SheetTitle } from \"@/components/ui/sheet\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { CopyButton } from \"@/components/elements/CopyButton\";\nimport { TagPicker } from \"@/components/elements/TagPicker\";\nimport { DatePicker } from \"@/components/elements/DatePicker\";\nimport { toast } from \"sonner\";\nimport { Flag, Loader2, Trash2 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { useTodoData } from \"@/context/TodoDataContext\";\nimport { useTags, useTagMutations, useTodoTags } from \"@/hooks/useTags\";\nimport { PRIORITY_LABELS, dueLabel } from \"@/lib/todos\";\nimport type { TodoDto } from \"@/lib/api\";\n\ninterface Props {\n  todo: TodoDto | null;\n  onClose: () => void;\n}\n\nconst NO_PRIORITY = \"none\";\n\nexport function TodoDetailSheet({ todo, onClose }: Props) {\n  const { lists, updateTodo, deleteTodo, toggleTodo } = useTodoData();\n  const { data: allTags = [] } = useTags();\n  const { create: createTag } = useTagMutations();\n  const { data: todoTags = [], setTags } = useTodoTags(todo?.id);\n  const [title, setTitle] = useState(\"\");\n  const [notes, setNotes] = useState(\"\");\n  const [dueDate, setDueDate] = useState(\"\");\n  const [priority, setPriority] = useState<string>(NO_PRIORITY);\n  const [flagged, setFlagged] = useState(false);\n  const [listId, setListId] = useState(\"\");\n  const [saving, setSaving] = useState(false);\n\n  useEffect(() => {\n    if (!todo) return;\n    setTitle(todo.title);\n    setNotes(todo.notes ?? \"\");\n    setDueDate(todo.dueDate ?? \"\");\n    setPriority(todo.priority ?? NO_PRIORITY);\n    setFlagged(todo.flagged);\n    setListId(todo.listId ?? \"\");\n  }, [todo]);\n\n  if (!todo) return null;\n\n  const save = async (updates: Partial<TodoDto>) => {\n    setSaving(true);\n    try {\n      await updateTodo(todo.id, updates as never);\n    } catch {\n      toast.error(\"Couldn't save that change\");\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  const commitTitle = () => {\n    const trimmed = title.trim();\n    if (!trimmed) { setTitle(todo.title); return; }\n    if (trimmed !== todo.title) void save({ title: trimmed });\n  };\n\n  const commitNotes = () => {\n    const next = notes.trim() || null;\n    if (next !== (todo.notes ?? null)) void save({ notes: next as never });\n  };\n\n  return (\n    <Sheet open={!!todo} onOpenChange={(open) => !open && onClose()}>\n      <SheetContent className=\"w-full sm:max-w-md overflow-y-auto\">\n        <SheetHeader>\n          <SheetTitle className=\"flex items-center gap-2\">\n            Todo\n            {todo.source !== \"manual\" && (\n              <Badge variant=\"outline\" className=\"text-[10px] font-normal\">via Shortcuts</Badge>\n            )}\n            {saving && <Loader2 size={13} className=\"animate-spin text-muted-foreground\" />}\n          </SheetTitle>\n        </SheetHeader>\n\n        <div className=\"space-y-5 py-4\">\n          <div className=\"flex items-start gap-2\">\n            <button\n              type=\"button\"\n              aria-label={todo.completed ? \"Mark as not done\" : \"Mark as done\"}\n              onClick={() => void toggleTodo(todo).catch(() => toast.error(\"Couldn't update that\"))}\n              className={cn(\n                \"mt-1 h-5 w-5 shrink-0 rounded-full border-2 flex items-center justify-center transition-colors\",\n                todo.completed ? \"bg-primary border-primary\" : \"border-muted-foreground/40 hover:border-primary\"\n              )}\n            >\n              {todo.completed && <span className=\"h-2 w-2 rounded-full bg-primary-foreground\" />}\n            </button>\n            <div className=\"flex-1 min-w-0\">\n              <Textarea\n                value={title}\n                onChange={(e) => setTitle(e.target.value)}\n                onBlur={commitTitle}\n                rows={1}\n                className={cn(\"min-h-0 resize-none text-base font-medium\", todo.completed && \"line-through text-muted-foreground\")}\n              />\n              {todo.completed && todo.completedAt && (\n                <p className=\"text-xs text-muted-foreground mt-1\">Completed {dueLabel(todo.completedAt)}</p>\n              )}\n            </div>\n          </div>\n\n          <div className=\"space-y-1.5\">\n            <Label className=\"text-xs text-muted-foreground\">Notes</Label>\n            <Textarea\n              value={notes}\n              onChange={(e) => setNotes(e.target.value)}\n              onBlur={commitNotes}\n              placeholder=\"Add notes…\"\n              rows={3}\n              className=\"text-sm\"\n            />\n          </div>\n\n          <div className=\"grid grid-cols-2 gap-3\">\n            <div className=\"space-y-1.5\">\n              <Label className=\"text-xs text-muted-foreground\">Due date</Label>\n              <DatePicker\n                value={dueDate || null}\n                onChange={(d) => { setDueDate(d ?? \"\"); void save({ dueDate: d as never }); }}\n                clearable\n              />\n            </div>\n            <div className=\"space-y-1.5\">\n              <Label className=\"text-xs text-muted-foreground\">Priority</Label>\n              <Select\n                value={priority}\n                onValueChange={(v) => { setPriority(v); void save({ priority: (v === NO_PRIORITY ? null : v) as never }); }}\n              >\n                <SelectTrigger><SelectValue /></SelectTrigger>\n                <SelectContent>\n                  <SelectItem value={NO_PRIORITY}>None</SelectItem>\n                  {Object.entries(PRIORITY_LABELS).map(([value, label]) => (\n                    <SelectItem key={value} value={value}>{label}</SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n          </div>\n\n          <div className=\"space-y-1.5\">\n            <Label className=\"text-xs text-muted-foreground\">List</Label>\n            <Select\n              value={listId}\n              onValueChange={(v) => { setListId(v); void save({ listId: v as never }); }}\n            >\n              <SelectTrigger><SelectValue /></SelectTrigger>\n              <SelectContent>\n                {lists.map((l) => (\n                  <SelectItem key={l.id} value={l.id}>{l.name}</SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n\n          <div className=\"space-y-1.5\">\n            <Label className=\"text-xs text-muted-foreground\">Tags</Label>\n            <TagPicker\n              tags={allTags}\n              selectedIds={todoTags.map((t) => t.id)}\n              onChange={(ids) => setTags.mutate(ids)}\n              onCreate={(name) => createTag.mutateAsync({ name }).then((t) => t.id)}\n              disabled={setTags.isPending}\n            />\n          </div>\n\n          <button\n            type=\"button\"\n            onClick={() => { setFlagged(!flagged); void save({ flagged: !flagged as never }); }}\n            className={cn(\n              \"flex items-center gap-2 text-sm rounded-md border px-3 py-2 transition-colors\",\n              flagged ? \"border-chart-orange/40 bg-chart-orange/10 text-chart-orange\" : \"border-border text-muted-foreground hover:text-foreground\"\n            )}\n          >\n            <Flag size={14} strokeWidth={1.5} fill={flagged ? \"currentColor\" : \"none\"} />\n            {flagged ? \"Flagged\" : \"Flag this todo\"}\n          </button>\n\n          <div className=\"space-y-1.5\">\n            <Label className=\"text-xs text-muted-foreground\">API URL</Label>\n            <div className=\"flex items-center gap-2\">\n              <code className=\"flex-1 text-xs bg-muted rounded-md px-3 py-2 overflow-x-auto whitespace-nowrap\">\n                {todo.url}\n              </code>\n              <CopyButton value={todo.url} label=\"Copy API URL\" />\n            </div>\n          </div>\n\n          <Button\n            variant=\"outline\"\n            className=\"w-full gap-2 text-destructive hover:text-destructive\"\n            onClick={() => {\n              void deleteTodo(todo.id).catch(() => toast.error(\"Couldn't delete that\"));\n              onClose();\n            }}\n          >\n            <Trash2 size={14} strokeWidth={1.5} /> Delete todo\n          </Button>\n        </div>\n      </SheetContent>\n    </Sheet>\n  );\n}\n"
    },
    {
      "name": "src/components/transactions/AccountBalanceChart.tsx",
      "type": "text/plain",
      "content": "import { Card, CardContent, CardHeader, CardTitle, CardDescription } from \"@/components/ui/card\";\nimport { TimeSeriesChart } from \"@/components/charts/TimeSeriesChart\";\nimport { SegmentedControl } from \"@/components/charts/SegmentedControl\";\nimport { ChartTypeToggle } from \"@/components/charts/ChartTypeToggle\";\nimport { CalendarDays, LineChart } from \"lucide-react\";\nimport { useFinanceData } from \"@/context/FinanceDataContext\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { toDateString } from \"@/lib/format\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { AnimatedMoney } from \"@/components/elements/AnimatedMoney\";\nimport { accountDisplayName } from \"@/lib/accounts\";\n\nconst periods = [\n  { label: \"30 days\", days: 30 },\n  { label: \"3 months\", days: 90 },\n  { label: \"6 months\", days: 180 },\n  { label: \"All time\", days: 999 },\n];\nconst periodOptions = periods.map((p) => ({ value: p.label, label: p.label }));\n\ninterface AccountBalanceChartProps {\n  /** Accounts whose balances are summed into the chart. Empty = nothing selected. */\n  accountIds: string[];\n  /** Optional control rendered in the header (e.g. an account multi-select). */\n  control?: React.ReactNode;\n  /** Transactions supplies the linked heatmap; account detail retains its line-only view. */\n  heatmap?: React.ReactNode;\n  lineChart?: React.ReactNode;\n  chartControls?: React.ReactNode;\n  /** Optional companion chart, taking one fifth of the card on wide screens. */\n  sideChart?: React.ReactNode;\n  chartType?: \"line\" | \"heatmap\";\n  onChartTypeChange?: (type: \"line\" | \"heatmap\") => void;\n  /** True when every selected account's balance represents money owed\n   * (credit cards, loans): plots the debt magnitude instead of the raw\n   * (negative) balance, so the line rises as debt grows, in red — more debt\n   * is unfavorable, the same convention Sure's credit card detail uses. */\n  debt?: boolean;\n}\n\n/**\n * Balance-over-time for a chosen set of accounts. Mirrors the dashboard\n * BalanceChart math (anchor the curve to today's real balance, back-compute\n * history from signed daily flow) but scoped to the selected accounts so each\n * bank account shows \"how much money is in it\" rather than just transactions.\n */\nexport function AccountBalanceChart({ accountIds, control, debt, heatmap, lineChart, chartControls, sideChart, chartType = \"line\", onChartTypeChange }: AccountBalanceChartProps) {\n  const { transactions, accounts } = useFinanceData();\n  const [period, setPeriod] = useState(\"3 months\");\n  const { compact, money, date: fmtDate } = useMoney();\n\n  const idSet = useMemo(() => new Set(accountIds), [accountIds]);\n  const selectedAccounts = useMemo(() => accounts.filter((a) => idSet.has(a.id)), [accounts, idSet]);\n\n  // Today's balance for the selected accounts = the number the chart ends on.\n  const currentBalance = useMemo(\n    () => selectedAccounts.reduce((s, a) => s + a.balance, 0),\n    [selectedAccounts]\n  );\n\n  const allData = useMemo(() => {\n    if (accountIds.length === 0) return [];\n    // A transaction with no date (a malformed import/sync row) can't be\n    // placed on a timeline; keeping it would crash the sort below.\n    const txns = transactions.filter((t) => idSet.has(t.accountId) && t.date);\n    if (txns.length === 0) return [];\n    const sorted = [...txns].sort((a, b) => a.date.localeCompare(b.date));\n    const dailyMap: Record<string, number> = {};\n    sorted.forEach((t) => {\n      const delta = t.type === \"income\" ? Math.abs(t.amount) : -Math.abs(t.amount);\n      dailyMap[t.date] = (dailyMap[t.date] || 0) + delta;\n    });\n    const days = Object.keys(dailyMap).sort();\n    let cumulative = 0;\n    const flow = days.map((date) => {\n      cumulative += dailyMap[date];\n      return { date, cumulative };\n    });\n    // Shift the running flow so its final point equals today's real balance.\n    const offset = currentBalance - (flow.length ? flow[flow.length - 1].cumulative : 0);\n    return flow.map(({ date, cumulative }) => ({ date, value: Math.round((cumulative + offset) * 100) / 100 }));\n  }, [transactions, idSet, accountIds, currentBalance]);\n\n  const data = useMemo(() => {\n    const days = periods.find((p) => p.label === period)?.days;\n    // Filter by a real date window. Slicing the array by `days` counted data\n    // points, so \"30 days\" over sparse data silently showed several months.\n    if (!days || days >= 999) return allData;\n    const cutoff = new Date();\n    cutoff.setDate(cutoff.getDate() - days);\n    const cutoffStr = toDateString(cutoff);\n    return allData.filter((d) => d.date >= cutoffStr);\n  }, [allData, period]);\n\n  const subtitle =\n    selectedAccounts.length === 0\n      ? \"No accounts selected\"\n      : selectedAccounts.length === 1\n        ? accountDisplayName(selectedAccounts[0])\n        : `${selectedAccounts.length} accounts combined`;\n\n  const displayBalance = debt ? -currentBalance : currentBalance;\n  const chartData = useMemo(() => (debt ? data.map((d) => ({ ...d, value: -d.value })) : data), [data, debt]);\n  const calendarView = !!heatmap && chartType === \"heatmap\";\n\n  return (\n    <Card className={cn(\"border-border/50\", sideChart && \"xl:grid xl:grid-cols-[minmax(0,4fr)_minmax(0,1fr)]\")} role=\"region\" aria-label={debt ? \"Balance owed\" : \"Current balance\"}>\n      <div className=\"min-w-0\">\n        <CardHeader className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-start sm:justify-between pb-2\">\n          <div className=\"space-y-1\">\n            <CardDescription className=\"text-[10px] uppercase tracking-wider font-semibold\">\n              {debt ? \"Balance owed\" : \"Current balance\"}\n            </CardDescription>\n            <CardTitle className={cn(\"text-3xl font-bold tabular-nums tracking-tight\", debt || currentBalance < 0 ? \"text-chart-red\" : \"text-foreground\")}>\n              <AnimatedMoney value={displayBalance} />\n            </CardTitle>\n            <p className=\"text-xs text-muted-foreground\">{subtitle}</p>\n          </div>\n          <div className=\"flex flex-col items-end gap-2\">\n            {control}\n            <div className=\"flex flex-wrap justify-end gap-2\">\n              {heatmap && onChartTypeChange && <ChartTypeToggle label=\"Current balance chart type\" options={[{ value: \"line\", label: \"Line\", icon: LineChart }, { value: \"heatmap\", label: \"Calendar heatmap\", icon: CalendarDays }]} value={chartType} onChange={onChartTypeChange} />}\n              {chartControls ?? (!calendarView && <SegmentedControl label=\"Chart period\" options={periodOptions} value={period} onChange={setPeriod} />)}\n            </div>\n          </div>\n        </CardHeader>\n        <CardContent>\n          {calendarView ? heatmap : lineChart ?? (chartData.length === 0 ? (\n            <p className=\"text-sm text-muted-foreground py-12 text-center\">\n              {accountIds.length === 0\n                ? \"Select at least one account to see its balance trend.\"\n                : \"No transactions for the selected account(s) to chart.\"}\n            </p>\n          ) : (\n            <TimeSeriesChart\n              data={chartData}\n              height={240}\n              label={debt ? \"Owed\" : \"Balance\"}\n              formatValue={money}\n              formatAxisValue={compact}\n              formatDate={(d) => fmtDate(d)}\n              yAxisWidth={44}\n              colorVar={debt ? \"--chart-red\" : undefined}\n            />\n          ))}\n        </CardContent>\n      </div>\n      {sideChart && <div className=\"flex min-w-0 items-center border-t border-border/50 p-5 xl:border-l xl:border-t-0\">{sideChart}</div>}\n    </Card>\n  );\n}\n"
    },
    {
      "name": "src/components/transactions/AddTransactionDialog.tsx",
      "type": "text/plain",
      "content": "import { useState } from \"react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Button } from \"@/components/ui/button\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport { Calendar as CalendarIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { toDateString } from \"@/lib/format\";\nimport { UNCATEGORIZED } from \"@/lib/categories\";\nimport { toast } from \"sonner\";\nimport type { Account, Transaction } from \"@/types/finance\";\nimport { CategoryPicker } from \"@/components/elements/CategoryPicker\";\nimport { accountDisplayName } from \"@/lib/accounts\";\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  accounts: Account[];\n  onAdd: (t: Transaction) => Promise<void>;\n}\n\n/**\n * Record a transaction by hand.\n *\n * The app had no way to add one at all, every transaction had to arrive through\n * Plaid, which makes cash spending and any non-linked account invisible.\n */\nexport function AddTransactionDialog({ open, onOpenChange, accounts, onAdd }: Props) {\n  const [date, setDate] = useState<Date>(new Date());\n  const [description, setDescription] = useState(\"\");\n  const [amount, setAmount] = useState(\"\");\n  const [type, setType] = useState<\"expense\" | \"income\">(\"expense\");\n  const [category, setCategory] = useState<string>(UNCATEGORIZED);\n  const [accountId, setAccountId] = useState(accounts[0]?.id ?? \"\");\n  const [saving, setSaving] = useState(false);\n\n  const reset = () => {\n    setDate(new Date());\n    setDescription(\"\");\n    setAmount(\"\");\n    setType(\"expense\");\n    setCategory(UNCATEGORIZED);\n    setAccountId(accounts[0]?.id ?? \"\");\n  };\n\n  const submit = async () => {\n    const desc = description.trim();\n    const magnitude = Math.abs(parseFloat(amount));\n    if (!desc) { toast.error(\"Add a description\"); return; }\n    if (!magnitude || Number.isNaN(magnitude)) { toast.error(\"Enter an amount greater than zero\"); return; }\n    if (!accountId) { toast.error(\"Choose an account\"); return; }\n\n    const account = accounts.find((a) => a.id === accountId);\n    setSaving(true);\n    try {\n      await onAdd({\n        // `manual-` keeps it clear of the `plaid-` prefix the sync deletes and\n        // rebuilds on every run, otherwise a hand-entered row would vanish.\n        id: `manual-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n        date: toDateString(date),\n        description: desc,\n        category,\n        // Expenses are stored negative, income positive, matching the Plaid sync.\n        amount: type === \"expense\" ? -magnitude : magnitude,\n        accountId,\n        accountName: account ? accountDisplayName(account) : \"Account\",\n        type,\n      });\n      toast.success(\"Transaction added\");\n      reset();\n      onOpenChange(false);\n    } catch {\n      toast.error(\"Couldn't save the transaction\");\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={(o) => { if (!o) reset(); onOpenChange(o); }}>\n      <DialogContent className=\"sm:max-w-[440px]\">\n        <DialogHeader><DialogTitle>Add transaction</DialogTitle></DialogHeader>\n        <div className=\"space-y-4 py-2\">\n          <div className=\"space-y-1.5\">\n            <Label>Description</Label>\n            <Input\n              value={description}\n              onChange={(e) => setDescription(e.target.value)}\n              placeholder=\"e.g. Farmers market\"\n              autoFocus\n            />\n          </div>\n\n          <div className=\"grid grid-cols-2 gap-3\">\n            <div className=\"space-y-1.5\">\n              <Label>Type</Label>\n              <Select value={type} onValueChange={(v) => setType(v as \"expense\" | \"income\")}>\n                <SelectTrigger><SelectValue /></SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"expense\">Expense</SelectItem>\n                  <SelectItem value=\"income\">Income</SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n            <div className=\"space-y-1.5\">\n              <Label>Amount</Label>\n              <Input\n                type=\"number\" min=\"0\" step=\"0.01\" placeholder=\"0.00\"\n                value={amount} onChange={(e) => setAmount(e.target.value)}\n                onKeyDown={(e) => e.key === \"Enter\" && void submit()}\n              />\n            </div>\n          </div>\n\n          <div className=\"grid grid-cols-2 gap-3\">\n            <div className=\"space-y-1.5\">\n              <Label>Date</Label>\n              <Popover>\n                <PopoverTrigger asChild>\n                  <Button variant=\"outline\" className={cn(\"w-full justify-start text-left font-normal\")}>\n                    <CalendarIcon className=\"mr-2 h-4 w-4\" />\n                    {toDateString(date)}\n                  </Button>\n                </PopoverTrigger>\n                <PopoverContent className=\"w-auto p-0 z-[100]\" align=\"start\">\n                  <Calendar\n                    mode=\"single\"\n                    selected={date}\n                    onSelect={(d) => d && setDate(d)}\n                    disabled={(d) => d > new Date()}\n                    initialFocus\n                  />\n                </PopoverContent>\n              </Popover>\n            </div>\n            <div className=\"space-y-1.5\">\n              <Label>Category</Label>\n              <CategoryPicker value={category} onChange={setCategory} />\n            </div>\n          </div>\n\n          <div className=\"space-y-1.5\">\n            <Label>Account</Label>\n            <Select value={accountId} onValueChange={setAccountId}>\n              <SelectTrigger><SelectValue placeholder=\"Choose an account\" /></SelectTrigger>\n              <SelectContent>\n                {accounts.map((a) => (\n                  <SelectItem key={a.id} value={a.id}>{accountDisplayName(a)}</SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n        </div>\n        <DialogFooter>\n          <Button variant=\"outline\" onClick={() => onOpenChange(false)}>Cancel</Button>\n          <Button onClick={() => void submit()} disabled={saving}>\n            {saving ? \"Saving…\" : \"Add\"}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
    },
    {
      "name": "src/components/transactions/DuplicateBadge.tsx",
      "type": "text/plain",
      "content": "import { Copy, Check, X } from \"lucide-react\";\nimport type { DuplicateDto } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The badge a possible-duplicate row gets, right next to (or in place of)\n * its category badge - same inline confirm/reject shape as TransferBadge,\n * for the same reason: the decision belongs right where the row already is,\n * not behind a trip to a separate page.\n */\nexport function DuplicateBadge({\n  suggested,\n  onMerge,\n  onDismiss,\n  busy,\n}: {\n  /** The pending DuplicateCandidate this row is a side of. */\n  suggested: DuplicateDto;\n  onMerge: () => void;\n  onDismiss: () => void;\n  busy?: boolean;\n}) {\n  return (\n    <span className=\"inline-flex items-center gap-1 text-[11px] pl-2 pr-1 py-0.5 rounded-full border bg-chart-yellow/10 text-chart-yellow border-chart-yellow/30\">\n      <Copy size={11} strokeWidth={1.75} className=\"shrink-0\" aria-hidden />\n      Duplicate?\n      <button\n        type=\"button\"\n        disabled={busy}\n        aria-label=\"Merge - delete this duplicate\"\n        title=\"Merge - delete this duplicate\"\n        onClick={(e) => { e.stopPropagation(); onMerge(); }}\n        className={cn(\"rounded-full p-0.5 hover:bg-chart-yellow/20 transition-colors\", busy && \"opacity-50\")}\n      >\n        <Check size={11} strokeWidth={2} />\n      </button>\n      <button\n        type=\"button\"\n        disabled={busy}\n        aria-label=\"Not a duplicate, keep both\"\n        title=\"Not a duplicate, keep both\"\n        onClick={(e) => { e.stopPropagation(); onDismiss(); }}\n        className={cn(\"rounded-full p-0.5 hover:bg-destructive/20 hover:text-destructive transition-colors\", busy && \"opacity-50\")}\n      >\n        <X size={11} strokeWidth={2} />\n      </button>\n    </span>\n  );\n}\n"
    },
    {
      "name": "src/components/transactions/SplitBadge.tsx",
      "type": "text/plain",
      "content": "import { Split } from \"lucide-react\";\n\n/** The badge a split transaction gets in the ledger, in place of its\n * category badge: `category` on the row itself is stale once the amount is\n * divided, so naming one category there would be misleading. */\nexport function SplitBadge({ count }: { count: number }) {\n  return (\n    <span className=\"inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full border bg-muted/60 text-muted-foreground border-border\">\n      <Split size={11} strokeWidth={1.75} className=\"shrink-0\" aria-hidden />\n      Split · {count}\n    </span>\n  );\n}\n"
    },
    {
      "name": "src/components/transactions/TransactionsActivityChart.tsx",
      "type": "text/plain",
      "content": "import { useMemo } from \"react\";\nimport { eachDayOfInterval, format } from \"date-fns\";\nimport { TimeSeriesChart } from \"@/components/charts/TimeSeriesChart\";\nimport { parseLocalDate } from \"@/lib/utils\";\nimport { CalendarHeatmap } from \"@/components/charts/CalendarHeatmap\";\nimport { Button } from \"@/components/ui/button\";\nimport { useTransactionActivity } from \"@/hooks/useTransactionActivity\";\nimport type { TransactionFilters } from \"@/hooks/useTransactionSearch\";\nimport { useMoney } from \"@/hooks/useMoney\";\nimport { toDateString } from \"@/lib/format\";\nimport type { CalendarSelection, FlowDirection } from \"@/lib/calendarActivity\";\nimport type { Account } from \"@/types/finance\";\n\ninterface Props {\n  filters: TransactionFilters;\n  accounts: Account[];\n  year: number;\n  direction: FlowDirection;\n  chartType: \"line\" | \"heatmap\";\n  periodDays: number;\n  onSelect: (selection: CalendarSelection | null) => void;\n}\n\n/** A chart-only view; search, dates, totals and the ledger belong to Transactions. */\nexport function TransactionsActivityChart({ filters, accounts, year, direction, chartType, periodDays, onSelect }: Props) {\n  const from = filters.dateFrom ? toDateString(filters.dateFrom) : undefined;\n  const to = filters.dateTo ? toDateString(filters.dateTo) : undefined;\n  const selection = from || to ? { from: from ?? `${year}-01-01`, to: to ?? `${year}-12-31` } : null;\n  const selectedAccounts = useMemo(() => accounts.filter(a => filters.accountIds?.includes(a.id) ?? (filters.accountId === \"all\" || a.id === filters.accountId)), [accounts, filters.accountIds, filters.accountId]);\n  const units = [...new Set(selectedAccounts.map(a => a.currency || \"XXX\"))];\n  const currency = units.length === 1 ? units[0] : undefined;\n  const { money, compact, date: fmtDate } = useMoney(currency);\n  const activity = useTransactionActivity(filters, year, chartType === \"line\" ? periodDays : undefined);\n  const lineData = useMemo(() => {\n    if (chartType !== \"line\" || activity.days.size === 0) return [];\n    const dates = [...activity.days.keys()].sort();\n    const end = periodDays ? new Date() : parseLocalDate(dates[dates.length - 1]);\n    const start = periodDays ? new Date(end.getFullYear(), end.getMonth(), end.getDate() - periodDays + 1) : parseLocalDate(dates[0]);\n    return eachDayOfInterval({ start, end }).map(day => {\n      const date = toDateString(day);\n      return { date, value: activity.days.get(date)?.[direction] ?? 0 };\n    });\n  }, [activity.days, chartType, periodDays, direction]);\n\n  return (\n    <section aria-label={chartType === \"heatmap\" ? \"Transaction activity heatmap\" : `Daily ${direction} line chart`} className=\"min-w-0\">\n      {selectedAccounts.length === 0 ? <p className=\"py-10 text-center text-sm text-muted-foreground\">Select at least one account to see daily activity.</p>\n        : !currency || currency === \"XXX\" ? <p className=\"py-10 text-center text-sm text-muted-foreground\">Select accounts with the same confirmed currency to compare daily amounts.</p>\n        : activity.error ? <div role=\"alert\" className=\"py-8 text-center\"><p className=\"text-sm text-muted-foreground\">Couldn't load daily activity.</p><Button size=\"sm\" variant=\"ghost\" onClick={() => void activity.retry()}>Retry activity</Button></div>\n        : activity.loading ? <div role=\"status\" className=\"flex h-44 items-center justify-center text-sm text-muted-foreground\">Loading daily activity…</div>\n        : chartType === \"line\" ? (lineData.length ? <TimeSeriesChart data={lineData} height={240} label={direction === \"outflow\" ? \"Outflow\" : \"Inflow\"} formatValue={money} formatAxisValue={compact} formatDate={fmtDate} yAxisWidth={44} zeroBaseline /> : <p className=\"py-12 text-center text-sm text-muted-foreground\">No transactions for the selected period.</p>)\n        : <CalendarHeatmap key={year} year={year} days={activity.days} direction={direction} selection={selection} onSelect={onSelect} formatValue={money} today={toDateString(new Date())} allowFuture />}\n      <p className=\"sr-only\">{from ? `From ${format(filters.dateFrom!, \"MMMM d, yyyy\")}. ` : \"\"}{to ? `Through ${format(filters.dateTo!, \"MMMM d, yyyy\")}.` : \"\"}</p>\n    </section>\n  );\n}\n"
    },
    {
      "name": "src/components/transactions/TransactionsBalanceChart.tsx",
      "type": "text/plain",
      "content": "import { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { SegmentedControl } from \"@/components/charts/SegmentedControl\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport type { TransactionFilters } from \"@/hooks/useTransactionSearch\";\nimport { calendarDate, type CalendarSelection, type FlowDirection } from \"@/lib/calendarActivity\";\nimport { toDateString } from \"@/lib/format\";\nimport type { Account, Transaction } from \"@/types/finance\";\nimport { AccountBalanceChart } from \"./AccountBalanceChart\";\nimport { TransactionsActivityChart } from \"./TransactionsActivityChart\";\n\nconst DIRECTIONS = [{ value: \"outflow\", label: \"Outflow\" }, { value: \"inflow\", label: \"Inflow\" }] as const;\nconst PERIODS = [{ days: 30, label: \"30 days\" }, { days: 90, label: \"3 months\" }, { days: 180, label: \"6 months\" }, { days: 0, label: \"All time\" }];\n\ninterface Props {\n  accountIds: string[];\n  accounts: Account[];\n  transactions: Transaction[];\n  filters: TransactionFilters;\n  chartType: \"line\" | \"heatmap\";\n  onChartTypeChange: (type: \"line\" | \"heatmap\") => void;\n  onSelect: (selection: CalendarSelection | null) => void;\n  control: ReactNode;\n  sideChart?: ReactNode;\n}\n\n/** One set of header controls for both daily-flow views; balance stays live. */\nexport function TransactionsBalanceChart({ accountIds, accounts, transactions, filters, chartType, onChartTypeChange, onSelect, control, sideChart }: Props) {\n  const [direction, setDirection] = useState<FlowDirection>(\"outflow\");\n  const [periodDays, setPeriodDays] = useState(90);\n  const [year, setYear] = useState(() => (filters.dateFrom ?? filters.dateTo ?? new Date()).getFullYear());\n  const from = filters.dateFrom ? toDateString(filters.dateFrom) : undefined;\n  const to = filters.dateTo ? toDateString(filters.dateTo) : undefined;\n  useEffect(() => {\n    const date = from ?? to;\n    if (date) setYear(Number(date.slice(0, 4)));\n  }, [from, to]);\n\n  const years = useMemo(() => {\n    const ids = new Set(accountIds);\n    return [...new Set([year, new Date().getFullYear(), ...transactions.flatMap(t => {\n      const date = calendarDate(t.date);\n      return ids.has(t.accountId) && date ? [Number(date.slice(0, 4))] : [];\n    })])].sort((a, b) => b - a);\n  }, [year, transactions, accountIds]);\n\n  // Both views share the chosen accounts, even when the table has no chart scope.\n  const activityFilters = { ...filters, accountId: \"all\", accountIds };\n  const chart = <TransactionsActivityChart filters={activityFilters} accounts={accounts} year={year} direction={direction} chartType={chartType} periodDays={periodDays} onSelect={onSelect} />;\n\n  return <AccountBalanceChart\n    accountIds={accountIds}\n    control={control}\n    sideChart={sideChart}\n    chartType={chartType}\n    onChartTypeChange={onChartTypeChange}\n    lineChart={chart}\n    heatmap={chart}\n    chartControls={<>\n      <SegmentedControl label=\"Current balance flow\" options={DIRECTIONS} value={direction} onChange={setDirection} />\n      {chartType === \"heatmap\" ? <Select value={String(year)} onValueChange={value => { setYear(Number(value)); onSelect(null); }}>\n        <SelectTrigger aria-label=\"Heatmap year\" className=\"h-8 w-[96px] text-xs\"><SelectValue /></SelectTrigger>\n        <SelectContent>{years.map(y => <SelectItem key={y} value={String(y)}>{y}</SelectItem>)}</SelectContent>\n      </Select> : <Select value={String(periodDays)} onValueChange={value => setPeriodDays(Number(value))}>\n        <SelectTrigger aria-label=\"Chart period\" className=\"h-8 w-[112px] text-xs\"><SelectValue /></SelectTrigger>\n        <SelectContent>{PERIODS.map(p => <SelectItem key={p.days} value={String(p.days)}>{p.label}</SelectItem>)}</SelectContent>\n      </Select>}\n    </>}\n  />;\n}\n"
    },
    {
      "name": "src/components/transactions/TransferBadge.tsx",
      "type": "text/plain",
      "content": "import { ArrowLeftRight, Check, X } from \"lucide-react\";\nimport type { TransferDto } from \"@/lib/api\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The badge a transfer-matched row gets in place of (or alongside) its\n * category badge. A still-`suggested` match carries inline confirm/reject\n * icons, matching Sure's \"Auto-matched badge\" pattern; a `confirmed` one is\n * just a plain label, there's nothing left to decide.\n */\nexport function TransferBadge({\n  suggested,\n  onConfirm,\n  onReject,\n  busy,\n}: {\n  /** The pending Transfer this row is a leg of, or undefined if it's already confirmed. */\n  suggested?: TransferDto;\n  onConfirm: () => void;\n  onReject: () => void;\n  busy?: boolean;\n}) {\n  if (!suggested) {\n    return (\n      <span className=\"inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full border bg-muted/60 text-muted-foreground border-border\">\n        <ArrowLeftRight size={11} strokeWidth={1.75} className=\"shrink-0\" aria-hidden />\n        Transfer\n      </span>\n    );\n  }\n  return (\n    <span className=\"inline-flex items-center gap-1 text-[11px] pl-2 pr-1 py-0.5 rounded-full border bg-primary/10 text-primary border-primary/30\">\n      <ArrowLeftRight size={11} strokeWidth={1.75} className=\"shrink-0\" aria-hidden />\n      Transfer?\n      <button\n        type=\"button\"\n        disabled={busy}\n        aria-label=\"Confirm this is a transfer\"\n        onClick={(e) => { e.stopPropagation(); onConfirm(); }}\n        className={cn(\"rounded-full p-0.5 hover:bg-primary/20 transition-colors\", busy && \"opacity-50\")}\n      >\n        <Check size={11} strokeWidth={2} />\n      </button>\n      <button\n        type=\"button\"\n        disabled={busy}\n        aria-label=\"Not a transfer\"\n        onClick={(e) => { e.stopPropagation(); onReject(); }}\n        className={cn(\"rounded-full p-0.5 hover:bg-destructive/20 hover:text-destructive transition-colors\", busy && \"opacity-50\")}\n      >\n        <X size={11} strokeWidth={2} />\n      </button>\n    </span>\n  );\n}\n"
    },
    {
      "name": "src/components/ui/accordion.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDown } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <AccordionPrimitive.Item ref={ref} className={cn(\"border-b\", className)} {...props} />\n));\nAccordionItem.displayName = \"AccordionItem\";\n\nconst AccordionTrigger = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Header className=\"flex\">\n    <AccordionPrimitive.Trigger\n      ref={ref}\n      className={cn(\n        \"flex flex-1 items-center justify-between py-4 font-medium transition-all [&[data-state=open]>svg]:rotate-180\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\n    </AccordionPrimitive.Trigger>\n  </AccordionPrimitive.Header>\n));\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;\n\nconst AccordionContent = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Content\n    ref={ref}\n    className=\"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\"\n    {...props}\n  >\n    <div className={cn(\"pb-4 pt-0\", className)}>{children}</div>\n  </AccordionPrimitive.Content>\n));\n\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
    },
    {
      "name": "src/components/ui/alert-dialog.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\n\nimport { cn } from \"@/lib/utils\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\nconst AlertDialog = AlertDialogPrimitive.Root;\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPortal>\n    <AlertDialogOverlay />\n    <AlertDialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className,\n      )}\n      {...props}\n    />\n  </AlertDialogPortal>\n));\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col space-y-2 text-center sm:text-left\", className)} {...props} />\n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nconst AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", className)} {...props} />\n);\nAlertDialogFooter.displayName = \"AlertDialogFooter\";\n\nconst AlertDialogTitle = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Title ref={ref} className={cn(\"text-lg font-semibold\", className)} {...props} />\n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Description ref={ref} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n));\nAlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogAction = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Action>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />\n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Cancel\n    ref={ref}\n    className={cn(buttonVariants({ variant: \"outline\" }), \"mt-2 sm:mt-0\", className)}\n    {...props}\n  />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n  AlertDialog,\n  AlertDialogPortal,\n  AlertDialogOverlay,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n};\n"
    },
    {
      "name": "src/components/ui/alert.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-background text-foreground\",\n        destructive: \"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst Alert = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n  <div ref={ref} role=\"alert\" className={cn(alertVariants({ variant }), className)} {...props} />\n));\nAlert.displayName = \"Alert\";\n\nconst AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(\n  ({ className, ...props }, ref) => (\n    <h5 ref={ref} className={cn(\"mb-1 font-medium leading-none tracking-tight\", className)} {...props} />\n  ),\n);\nAlertTitle.displayName = \"AlertTitle\";\n\nconst AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={cn(\"text-sm [&_p]:leading-relaxed\", className)} {...props} />\n  ),\n);\nAlertDescription.displayName = \"AlertDescription\";\n\nexport { Alert, AlertTitle, AlertDescription };\n"
    },
    {
      "name": "src/components/ui/aspect-ratio.tsx",
      "type": "text/plain",
      "content": "import * as AspectRatioPrimitive from \"@radix-ui/react-aspect-ratio\";\n\nconst AspectRatio = AspectRatioPrimitive.Root;\n\nexport { AspectRatio };\n"
    },
    {
      "name": "src/components/ui/avatar.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\", className)}\n    {...props}\n  />\n));\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image ref={ref} className={cn(\"aspect-square h-full w-full\", className)} {...props} />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\"flex h-full w-full items-center justify-center rounded-full bg-muted\", className)}\n    {...props}\n  />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
    },
    {
      "name": "src/components/ui/badge.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default: \"border-transparent bg-primary text-primary-foreground hover:bg-primary/80\",\n        secondary: \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive: \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return <div className={cn(badgeVariants({ variant }), className)} {...props} />;\n}\n\nexport { Badge, badgeVariants };\n"
    },
    {
      "name": "src/components/ui/breadcrumb.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Breadcrumb = React.forwardRef<\n  HTMLElement,\n  React.ComponentPropsWithoutRef<\"nav\"> & {\n    separator?: React.ReactNode;\n  }\n>(({ ...props }, ref) => <nav ref={ref} aria-label=\"breadcrumb\" {...props} />);\nBreadcrumb.displayName = \"Breadcrumb\";\n\nconst BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<\"ol\">>(\n  ({ className, ...props }, ref) => (\n    <ol\n      ref={ref}\n      className={cn(\n        \"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nBreadcrumbList.displayName = \"BreadcrumbList\";\n\nconst BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<\"li\">>(\n  ({ className, ...props }, ref) => (\n    <li ref={ref} className={cn(\"inline-flex items-center gap-1.5\", className)} {...props} />\n  ),\n);\nBreadcrumbItem.displayName = \"BreadcrumbItem\";\n\nconst BreadcrumbLink = React.forwardRef<\n  HTMLAnchorElement,\n  React.ComponentPropsWithoutRef<\"a\"> & {\n    asChild?: boolean;\n  }\n>(({ asChild, className, ...props }, ref) => {\n  const Comp = asChild ? Slot : \"a\";\n\n  return <Comp ref={ref} className={cn(\"transition-colors hover:text-foreground\", className)} {...props} />;\n});\nBreadcrumbLink.displayName = \"BreadcrumbLink\";\n\nconst BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<\"span\">>(\n  ({ className, ...props }, ref) => (\n    <span\n      ref={ref}\n      role=\"link\"\n      aria-disabled=\"true\"\n      aria-current=\"page\"\n      className={cn(\"font-normal text-foreground\", className)}\n      {...props}\n    />\n  ),\n);\nBreadcrumbPage.displayName = \"BreadcrumbPage\";\n\nconst BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<\"li\">) => (\n  <li role=\"presentation\" aria-hidden=\"true\" className={cn(\"[&>svg]:size-3.5\", className)} {...props}>\n    {children ?? <ChevronRight />}\n  </li>\n);\nBreadcrumbSeparator.displayName = \"BreadcrumbSeparator\";\n\nconst BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<\"span\">) => (\n  <span\n    role=\"presentation\"\n    aria-hidden=\"true\"\n    className={cn(\"flex h-9 w-9 items-center justify-center\", className)}\n    {...props}\n  >\n    <MoreHorizontal className=\"h-4 w-4\" />\n    <span className=\"sr-only\">More</span>\n  </span>\n);\nBreadcrumbEllipsis.displayName = \"BreadcrumbElipssis\";\n\nexport {\n  Breadcrumb,\n  BreadcrumbList,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n  BreadcrumbEllipsis,\n};\n"
    },
    {
      "name": "src/components/ui/button.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline: \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 px-3\",\n        lg: \"h-11 px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
    },
    {
      "name": "src/components/ui/calendar.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { DayPicker } from \"react-day-picker\";\n\nimport { cn } from \"@/lib/utils\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\nexport type CalendarProps = React.ComponentProps<typeof DayPicker>;\n\nfunction Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {\n  return (\n    <DayPicker\n      showOutsideDays={showOutsideDays}\n      className={cn(\"p-3\", className)}\n      classNames={{\n        months: \"flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0\",\n        month: \"space-y-4\",\n        caption: \"flex justify-center pt-1 relative items-center\",\n        caption_label: \"text-sm font-medium\",\n        nav: \"space-x-1 flex items-center\",\n        nav_button: cn(\n          buttonVariants({ variant: \"outline\" }),\n          \"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100\",\n        ),\n        nav_button_previous: \"absolute left-1\",\n        nav_button_next: \"absolute right-1\",\n        table: \"w-full border-collapse space-y-1\",\n        head_row: \"flex\",\n        head_cell: \"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]\",\n        row: \"flex w-full mt-2\",\n        cell: \"h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20\",\n        day: cn(buttonVariants({ variant: \"ghost\" }), \"h-9 w-9 p-0 font-normal aria-selected:opacity-100\"),\n        day_range_end: \"day-range-end\",\n        day_selected:\n          \"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground\",\n        day_today: \"bg-accent text-accent-foreground\",\n        day_outside:\n          \"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30\",\n        day_disabled: \"text-muted-foreground opacity-50\",\n        day_range_middle: \"aria-selected:bg-accent aria-selected:text-accent-foreground\",\n        day_hidden: \"invisible\",\n        ...classNames,\n      }}\n      components={{\n        IconLeft: ({ ..._props }) => <ChevronLeft className=\"h-4 w-4\" />,\n        IconRight: ({ ..._props }) => <ChevronRight className=\"h-4 w-4\" />,\n      }}\n      {...props}\n    />\n  );\n}\nCalendar.displayName = \"Calendar\";\n\nexport { Calendar };\n"
    },
    {
      "name": "src/components/ui/card.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"rounded-sm border bg-card text-card-foreground shadow-sm\", className)} {...props} />\n));\nCard.displayName = \"Card\";\n\nconst CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={cn(\"flex flex-col space-y-1.5 p-6\", className)} {...props} />\n  ),\n);\nCardHeader.displayName = \"CardHeader\";\n\nconst CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(\n  ({ className, ...props }, ref) => (\n    <h3 ref={ref} className={cn(\"text-2xl font-semibold leading-none tracking-tight\", className)} {...props} />\n  ),\n);\nCardTitle.displayName = \"CardTitle\";\n\nconst CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, ...props }, ref) => (\n    <p ref={ref} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n  ),\n);\nCardDescription.displayName = \"CardDescription\";\n\nconst CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />,\n);\nCardContent.displayName = \"CardContent\";\n\nconst CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={cn(\"flex items-center p-6 pt-0\", className)} {...props} />\n  ),\n);\nCardFooter.displayName = \"CardFooter\";\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };\n"
    },
    {
      "name": "src/components/ui/carousel.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport useEmblaCarousel, { type UseEmblaCarouselType } from \"embla-carousel-react\";\nimport { ArrowLeft, ArrowRight } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>;\ntype CarouselOptions = UseCarouselParameters[0];\ntype CarouselPlugin = UseCarouselParameters[1];\n\ntype CarouselProps = {\n  opts?: CarouselOptions;\n  plugins?: CarouselPlugin;\n  orientation?: \"horizontal\" | \"vertical\";\n  setApi?: (api: CarouselApi) => void;\n};\n\ntype CarouselContextProps = {\n  carouselRef: ReturnType<typeof useEmblaCarousel>[0];\n  api: ReturnType<typeof useEmblaCarousel>[1];\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n} & CarouselProps;\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null);\n\nfunction useCarousel() {\n  const context = React.useContext(CarouselContext);\n\n  if (!context) {\n    throw new Error(\"useCarousel must be used within a <Carousel />\");\n  }\n\n  return context;\n}\n\nconst Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & CarouselProps>(\n  ({ orientation = \"horizontal\", opts, setApi, plugins, className, children, ...props }, ref) => {\n    const [carouselRef, api] = useEmblaCarousel(\n      {\n        ...opts,\n        axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n      },\n      plugins,\n    );\n    const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n    const [canScrollNext, setCanScrollNext] = React.useState(false);\n\n    const onSelect = React.useCallback((api: CarouselApi) => {\n      if (!api) {\n        return;\n      }\n\n      setCanScrollPrev(api.canScrollPrev());\n      setCanScrollNext(api.canScrollNext());\n    }, []);\n\n    const scrollPrev = React.useCallback(() => {\n      api?.scrollPrev();\n    }, [api]);\n\n    const scrollNext = React.useCallback(() => {\n      api?.scrollNext();\n    }, [api]);\n\n    const handleKeyDown = React.useCallback(\n      (event: React.KeyboardEvent<HTMLDivElement>) => {\n        if (event.key === \"ArrowLeft\") {\n          event.preventDefault();\n          scrollPrev();\n        } else if (event.key === \"ArrowRight\") {\n          event.preventDefault();\n          scrollNext();\n        }\n      },\n      [scrollPrev, scrollNext],\n    );\n\n    React.useEffect(() => {\n      if (!api || !setApi) {\n        return;\n      }\n\n      setApi(api);\n    }, [api, setApi]);\n\n    React.useEffect(() => {\n      if (!api) {\n        return;\n      }\n\n      onSelect(api);\n      api.on(\"reInit\", onSelect);\n      api.on(\"select\", onSelect);\n\n      return () => {\n        api?.off(\"select\", onSelect);\n      };\n    }, [api, onSelect]);\n\n    return (\n      <CarouselContext.Provider\n        value={{\n          carouselRef,\n          api: api,\n          opts,\n          orientation: orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n          scrollPrev,\n          scrollNext,\n          canScrollPrev,\n          canScrollNext,\n        }}\n      >\n        <div\n          ref={ref}\n          onKeyDownCapture={handleKeyDown}\n          className={cn(\"relative\", className)}\n          role=\"region\"\n          aria-roledescription=\"carousel\"\n          {...props}\n        >\n          {children}\n        </div>\n      </CarouselContext.Provider>\n    );\n  },\n);\nCarousel.displayName = \"Carousel\";\n\nconst CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => {\n    const { carouselRef, orientation } = useCarousel();\n\n    return (\n      <div ref={carouselRef} className=\"overflow-hidden\">\n        <div\n          ref={ref}\n          className={cn(\"flex\", orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\", className)}\n          {...props}\n        />\n      </div>\n    );\n  },\n);\nCarouselContent.displayName = \"CarouselContent\";\n\nconst CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => {\n    const { orientation } = useCarousel();\n\n    return (\n      <div\n        ref={ref}\n        role=\"group\"\n        aria-roledescription=\"slide\"\n        className={cn(\"min-w-0 shrink-0 grow-0 basis-full\", orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\", className)}\n        {...props}\n      />\n    );\n  },\n);\nCarouselItem.displayName = \"CarouselItem\";\n\nconst CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(\n  ({ className, variant = \"outline\", size = \"icon\", ...props }, ref) => {\n    const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n\n    return (\n      <Button\n        ref={ref}\n        variant={variant}\n        size={size}\n        className={cn(\n          \"absolute h-8 w-8 rounded-full\",\n          orientation === \"horizontal\"\n            ? \"-left-12 top-1/2 -translate-y-1/2\"\n            : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n          className,\n        )}\n        disabled={!canScrollPrev}\n        onClick={scrollPrev}\n        {...props}\n      >\n        <ArrowLeft className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Previous slide</span>\n      </Button>\n    );\n  },\n);\nCarouselPrevious.displayName = \"CarouselPrevious\";\n\nconst CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(\n  ({ className, variant = \"outline\", size = \"icon\", ...props }, ref) => {\n    const { orientation, scrollNext, canScrollNext } = useCarousel();\n\n    return (\n      <Button\n        ref={ref}\n        variant={variant}\n        size={size}\n        className={cn(\n          \"absolute h-8 w-8 rounded-full\",\n          orientation === \"horizontal\"\n            ? \"-right-12 top-1/2 -translate-y-1/2\"\n            : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n          className,\n        )}\n        disabled={!canScrollNext}\n        onClick={scrollNext}\n        {...props}\n      >\n        <ArrowRight className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Next slide</span>\n      </Button>\n    );\n  },\n);\nCarouselNext.displayName = \"CarouselNext\";\n\nexport { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };\n"
    },
    {
      "name": "src/components/ui/checkbox.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { Check } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <CheckboxPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <CheckboxPrimitive.Indicator className={cn(\"flex items-center justify-center text-current\")}>\n      <Check className=\"h-4 w-4\" />\n    </CheckboxPrimitive.Indicator>\n  </CheckboxPrimitive.Root>\n));\nCheckbox.displayName = CheckboxPrimitive.Root.displayName;\n\nexport { Checkbox };\n"
    },
    {
      "name": "src/components/ui/collapsible.tsx",
      "type": "text/plain",
      "content": "import * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\";\n\nconst Collapsible = CollapsiblePrimitive.Root;\n\nconst CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;\n\nconst CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent };\n"
    },
    {
      "name": "src/components/ui/command.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport { type DialogProps } from \"@radix-ui/react-dialog\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Search } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\n\nconst Command = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nCommand.displayName = CommandPrimitive.displayName;\n\ntype CommandDialogProps = DialogProps;\n\nconst CommandDialog = ({ children, ...props }: CommandDialogProps) => {\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"overflow-hidden p-0 shadow-lg\">\n        <Command className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst CommandInput = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Input>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n  <div className=\"flex items-center border-b px-3\" cmdk-input-wrapper=\"\">\n    <Search className=\"mr-2 h-4 w-4 shrink-0 opacity-50\" />\n    <CommandPrimitive.Input\n      ref={ref}\n      className={cn(\n        \"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  </div>\n));\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.List\n    ref={ref}\n    className={cn(\"max-h-[300px] overflow-y-auto overflow-x-hidden\", className)}\n    {...props}\n  />\n));\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Empty>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => <CommandPrimitive.Empty ref={ref} className=\"py-6 text-center text-sm\" {...props} />);\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Group>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Group\n    ref={ref}\n    className={cn(\n      \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Separator ref={ref} className={cn(\"-mx-1 h-px bg-border\", className)} {...props} />\n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\nconst CommandItem = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {\n  return <span className={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", className)} {...props} />;\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n};\n"
    },
    {
      "name": "src/components/ui/context-menu.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\";\nimport { Check, ChevronRight, Circle } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst ContextMenu = ContextMenuPrimitive.Root;\n\nconst ContextMenuTrigger = ContextMenuPrimitive.Trigger;\n\nconst ContextMenuGroup = ContextMenuPrimitive.Group;\n\nconst ContextMenuPortal = ContextMenuPrimitive.Portal;\n\nconst ContextMenuSub = ContextMenuPrimitive.Sub;\n\nconst ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;\n\nconst ContextMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <ContextMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </ContextMenuPrimitive.SubTrigger>\n));\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;\n\nconst ContextMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;\n\nconst ContextMenuContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Portal>\n    <ContextMenuPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </ContextMenuPrimitive.Portal>\n));\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;\n\nconst ContextMenuItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <ContextMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground\",\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.CheckboxItem>\n));\nContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;\n\nconst ContextMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <ContextMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.RadioItem>\n));\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;\n\nconst ContextMenuLabel = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Label\n    ref={ref}\n    className={cn(\"px-2 py-1.5 text-sm font-semibold text-foreground\", inset && \"pl-8\", className)}\n    {...props}\n  />\n));\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;\n\nconst ContextMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Separator ref={ref} className={cn(\"-mx-1 my-1 h-px bg-border\", className)} {...props} />\n));\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;\n\nconst ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {\n  return <span className={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", className)} {...props} />;\n};\nContextMenuShortcut.displayName = \"ContextMenuShortcut\";\n\nexport {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuCheckboxItem,\n  ContextMenuRadioItem,\n  ContextMenuLabel,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuGroup,\n  ContextMenuPortal,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuRadioGroup,\n};\n"
    },
    {
      "name": "src/components/ui/dialog.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col space-y-1.5 text-center sm:text-left\", className)} {...props} />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", className)} {...props} />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold leading-none tracking-tight\", className)}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description ref={ref} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
    },
    {
      "name": "src/components/ui/drawer.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"vaul\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (\n  <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />\n);\nDrawer.displayName = \"Drawer\";\n\nconst DrawerTrigger = DrawerPrimitive.Trigger;\n\nconst DrawerPortal = DrawerPrimitive.Portal;\n\nconst DrawerClose = DrawerPrimitive.Close;\n\nconst DrawerOverlay = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Overlay ref={ref} className={cn(\"fixed inset-0 z-50 bg-black/80\", className)} {...props} />\n));\nDrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;\n\nconst DrawerContent = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DrawerPortal>\n    <DrawerOverlay />\n    <DrawerPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted\" />\n      {children}\n    </DrawerPrimitive.Content>\n  </DrawerPortal>\n));\nDrawerContent.displayName = \"DrawerContent\";\n\nconst DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"grid gap-1.5 p-4 text-center sm:text-left\", className)} {...props} />\n);\nDrawerHeader.displayName = \"DrawerHeader\";\n\nconst DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)} {...props} />\n);\nDrawerFooter.displayName = \"DrawerFooter\";\n\nconst DrawerTitle = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold leading-none tracking-tight\", className)}\n    {...props}\n  />\n));\nDrawerTitle.displayName = DrawerPrimitive.Title.displayName;\n\nconst DrawerDescription = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Description ref={ref} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n));\nDrawerDescription.displayName = DrawerPrimitive.Description.displayName;\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n};\n"
    },
    {
      "name": "src/components/ui/dropdown-menu.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { Check, ChevronRight, Circle } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent focus:bg-accent\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground\",\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\"px-2 py-1.5 text-sm font-semibold\", inset && \"pl-8\", className)}\n    {...props}\n  />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator ref={ref} className={cn(\"-mx-1 my-1 h-px bg-muted\", className)} {...props} />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {\n  return <span className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)} {...props} />;\n};\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\";\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
    },
    {
      "name": "src/components/ui/form.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from \"react-hook-form\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Label } from \"@/components/ui/label\";\n\nconst Form = FormProvider;\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);\n\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState, formState } = useFormContext();\n\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error(\"useFormField should be used within <FormField>\");\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);\n\nconst FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => {\n    const id = React.useId();\n\n    return (\n      <FormItemContext.Provider value={{ id }}>\n        <div ref={ref} className={cn(\"space-y-2\", className)} {...props} />\n      </FormItemContext.Provider>\n    );\n  },\n);\nFormItem.displayName = \"FormItem\";\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField();\n\n  return <Label ref={ref} className={cn(error && \"text-destructive\", className)} htmlFor={formItemId} {...props} />;\n});\nFormLabel.displayName = \"FormLabel\";\n\nconst FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(\n  ({ ...props }, ref) => {\n    const { error, formItemId, formDescriptionId, formMessageId } = useFormField();\n\n    return (\n      <Slot\n        ref={ref}\n        id={formItemId}\n        aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}\n        aria-invalid={!!error}\n        {...props}\n      />\n    );\n  },\n);\nFormControl.displayName = \"FormControl\";\n\nconst FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, ...props }, ref) => {\n    const { formDescriptionId } = useFormField();\n\n    return <p ref={ref} id={formDescriptionId} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />;\n  },\n);\nFormDescription.displayName = \"FormDescription\";\n\nconst FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, children, ...props }, ref) => {\n    const { error, formMessageId } = useFormField();\n    const body = error ? String(error?.message) : children;\n\n    if (!body) {\n      return null;\n    }\n\n    return (\n      <p ref={ref} id={formMessageId} className={cn(\"text-sm font-medium text-destructive\", className)} {...props}>\n        {body}\n      </p>\n    );\n  },\n);\nFormMessage.displayName = \"FormMessage\";\n\nexport { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };\n"
    },
    {
      "name": "src/components/ui/hover-card.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst HoverCard = HoverCardPrimitive.Root;\n\nconst HoverCardTrigger = HoverCardPrimitive.Trigger;\n\nconst HoverCardContent = React.forwardRef<\n  React.ElementRef<typeof HoverCardPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <HoverCardPrimitive.Content\n    ref={ref}\n    align={align}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nHoverCardContent.displayName = HoverCardPrimitive.Content.displayName;\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent };\n"
    },
    {
      "name": "src/components/ui/input-otp.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport { OTPInput, OTPInputContext } from \"input-otp\";\nimport { Dot } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst InputOTP = React.forwardRef<React.ElementRef<typeof OTPInput>, React.ComponentPropsWithoutRef<typeof OTPInput>>(\n  ({ className, containerClassName, ...props }, ref) => (\n    <OTPInput\n      ref={ref}\n      containerClassName={cn(\"flex items-center gap-2 has-[:disabled]:opacity-50\", containerClassName)}\n      className={cn(\"disabled:cursor-not-allowed\", className)}\n      {...props}\n    />\n  ),\n);\nInputOTP.displayName = \"InputOTP\";\n\nconst InputOTPGroup = React.forwardRef<React.ElementRef<\"div\">, React.ComponentPropsWithoutRef<\"div\">>(\n  ({ className, ...props }, ref) => <div ref={ref} className={cn(\"flex items-center\", className)} {...props} />,\n);\nInputOTPGroup.displayName = \"InputOTPGroup\";\n\nconst InputOTPSlot = React.forwardRef<\n  React.ElementRef<\"div\">,\n  React.ComponentPropsWithoutRef<\"div\"> & { index: number }\n>(({ index, className, ...props }, ref) => {\n  const inputOTPContext = React.useContext(OTPInputContext);\n  const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        \"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md\",\n        isActive && \"z-10 ring-2 ring-ring ring-offset-background\",\n        className,\n      )}\n      {...props}\n    >\n      {char}\n      {hasFakeCaret && (\n        <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n          <div className=\"animate-caret-blink h-4 w-px bg-foreground duration-1000\" />\n        </div>\n      )}\n    </div>\n  );\n});\nInputOTPSlot.displayName = \"InputOTPSlot\";\n\nconst InputOTPSeparator = React.forwardRef<React.ElementRef<\"div\">, React.ComponentPropsWithoutRef<\"div\">>(\n  ({ ...props }, ref) => (\n    <div ref={ref} role=\"separator\" {...props}>\n      <Dot />\n    </div>\n  ),\n);\nInputOTPSeparator.displayName = \"InputOTPSeparator\";\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };\n"
    },
    {
      "name": "src/components/ui/input.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Input = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
    },
    {
      "name": "src/components/ui/label.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst labelVariants = cva(\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\");\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
    },
    {
      "name": "src/components/ui/menubar.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as MenubarPrimitive from \"@radix-ui/react-menubar\";\nimport { Check, ChevronRight, Circle } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst MenubarMenu = MenubarPrimitive.Menu;\n\nconst MenubarGroup = MenubarPrimitive.Group;\n\nconst MenubarPortal = MenubarPrimitive.Portal;\n\nconst MenubarSub = MenubarPrimitive.Sub;\n\nconst MenubarRadioGroup = MenubarPrimitive.RadioGroup;\n\nconst Menubar = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Root\n    ref={ref}\n    className={cn(\"flex h-10 items-center space-x-1 rounded-md border bg-background p-1\", className)}\n    {...props}\n  />\n));\nMenubar.displayName = MenubarPrimitive.Root.displayName;\n\nconst MenubarTrigger = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-pointer select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nMenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;\n\nconst MenubarSubTrigger = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <MenubarPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </MenubarPrimitive.SubTrigger>\n));\nMenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;\n\nconst MenubarSubContent = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nMenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;\n\nconst MenubarContent = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>\n>(({ className, align = \"start\", alignOffset = -4, sideOffset = 8, ...props }, ref) => (\n  <MenubarPrimitive.Portal>\n    <MenubarPrimitive.Content\n      ref={ref}\n      align={align}\n      alignOffset={alignOffset}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </MenubarPrimitive.Portal>\n));\nMenubarContent.displayName = MenubarPrimitive.Content.displayName;\n\nconst MenubarItem = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <MenubarPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nMenubarItem.displayName = MenubarPrimitive.Item.displayName;\n\nconst MenubarCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <MenubarPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground\",\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <MenubarPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </MenubarPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </MenubarPrimitive.CheckboxItem>\n));\nMenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;\n\nconst MenubarRadioItem = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <MenubarPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <MenubarPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </MenubarPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </MenubarPrimitive.RadioItem>\n));\nMenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;\n\nconst MenubarLabel = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <MenubarPrimitive.Label\n    ref={ref}\n    className={cn(\"px-2 py-1.5 text-sm font-semibold\", inset && \"pl-8\", className)}\n    {...props}\n  />\n));\nMenubarLabel.displayName = MenubarPrimitive.Label.displayName;\n\nconst MenubarSeparator = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Separator ref={ref} className={cn(\"-mx-1 my-1 h-px bg-muted\", className)} {...props} />\n));\nMenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;\n\nconst MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {\n  return <span className={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", className)} {...props} />;\n};\nMenubarShortcut.displayname = \"MenubarShortcut\";\n\nexport {\n  Menubar,\n  MenubarMenu,\n  MenubarTrigger,\n  MenubarContent,\n  MenubarItem,\n  MenubarSeparator,\n  MenubarLabel,\n  MenubarCheckboxItem,\n  MenubarRadioGroup,\n  MenubarRadioItem,\n  MenubarPortal,\n  MenubarSubContent,\n  MenubarSubTrigger,\n  MenubarGroup,\n  MenubarSub,\n  MenubarShortcut,\n};\n"
    },
    {
      "name": "src/components/ui/navigation-menu.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\";\nimport { cva } from \"class-variance-authority\";\nimport { ChevronDown } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst NavigationMenu = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <NavigationMenuPrimitive.Root\n    ref={ref}\n    className={cn(\"relative z-10 flex max-w-max flex-1 items-center justify-center\", className)}\n    {...props}\n  >\n    {children}\n    <NavigationMenuViewport />\n  </NavigationMenuPrimitive.Root>\n));\nNavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;\n\nconst NavigationMenuList = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.List\n    ref={ref}\n    className={cn(\"group flex flex-1 list-none items-center justify-center space-x-1\", className)}\n    {...props}\n  />\n));\nNavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;\n\nconst NavigationMenuItem = NavigationMenuPrimitive.Item;\n\nconst navigationMenuTriggerStyle = cva(\n  \"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50\",\n);\n\nconst NavigationMenuTrigger = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <NavigationMenuPrimitive.Trigger\n    ref={ref}\n    className={cn(navigationMenuTriggerStyle(), \"group\", className)}\n    {...props}\n  >\n    {children}{\" \"}\n    <ChevronDown\n      className=\"relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180\"\n      aria-hidden=\"true\"\n    />\n  </NavigationMenuPrimitive.Trigger>\n));\nNavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;\n\nconst NavigationMenuContent = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto\",\n      className,\n    )}\n    {...props}\n  />\n));\nNavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;\n\nconst NavigationMenuLink = NavigationMenuPrimitive.Link;\n\nconst NavigationMenuViewport = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>\n>(({ className, ...props }, ref) => (\n  <div className={cn(\"absolute left-0 top-full flex justify-center\")}>\n    <NavigationMenuPrimitive.Viewport\n      className={cn(\n        \"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    />\n  </div>\n));\nNavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;\n\nconst NavigationMenuIndicator = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.Indicator\n    ref={ref}\n    className={cn(\n      \"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in\",\n      className,\n    )}\n    {...props}\n  >\n    <div className=\"relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md\" />\n  </NavigationMenuPrimitive.Indicator>\n));\nNavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;\n\nexport {\n  navigationMenuTriggerStyle,\n  NavigationMenu,\n  NavigationMenuList,\n  NavigationMenuItem,\n  NavigationMenuContent,\n  NavigationMenuTrigger,\n  NavigationMenuLink,\n  NavigationMenuIndicator,\n  NavigationMenuViewport,\n};\n"
    },
    {
      "name": "src/components/ui/pagination.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport { ChevronLeft, ChevronRight, MoreHorizontal } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ButtonProps, buttonVariants } from \"@/components/ui/button\";\n\nconst Pagination = ({ className, ...props }: React.ComponentProps<\"nav\">) => (\n  <nav\n    role=\"navigation\"\n    aria-label=\"pagination\"\n    className={cn(\"mx-auto flex w-full justify-center\", className)}\n    {...props}\n  />\n);\nPagination.displayName = \"Pagination\";\n\nconst PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(\n  ({ className, ...props }, ref) => (\n    <ul ref={ref} className={cn(\"flex flex-row flex-wrap items-center justify-center gap-1\", className)} {...props} />\n  ),\n);\nPaginationContent.displayName = \"PaginationContent\";\n\nconst PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(({ className, ...props }, ref) => (\n  <li ref={ref} className={cn(\"\", className)} {...props} />\n));\nPaginationItem.displayName = \"PaginationItem\";\n\ntype PaginationLinkProps = {\n  isActive?: boolean;\n} & Pick<ButtonProps, \"size\"> &\n  React.ComponentProps<\"a\">;\n\nconst PaginationLink = ({ className, isActive, size = \"icon\", ...props }: PaginationLinkProps) => (\n  <a\n    aria-current={isActive ? \"page\" : undefined}\n    className={cn(\n      buttonVariants({\n        variant: isActive ? \"outline\" : \"ghost\",\n        size,\n      }),\n      className,\n    )}\n    {...props}\n  />\n);\nPaginationLink.displayName = \"PaginationLink\";\n\nconst PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink aria-label=\"Go to previous page\" size=\"default\" className={cn(\"gap-1 pl-2.5\", className)} {...props}>\n    <ChevronLeft className=\"h-4 w-4\" />\n    <span>Previous</span>\n  </PaginationLink>\n);\nPaginationPrevious.displayName = \"PaginationPrevious\";\n\nconst PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink aria-label=\"Go to next page\" size=\"default\" className={cn(\"gap-1 pr-2.5\", className)} {...props}>\n    <span>Next</span>\n    <ChevronRight className=\"h-4 w-4\" />\n  </PaginationLink>\n);\nPaginationNext.displayName = \"PaginationNext\";\n\nconst PaginationEllipsis = ({ className, ...props }: React.ComponentProps<\"span\">) => (\n  <span aria-hidden className={cn(\"flex h-9 w-9 items-center justify-center\", className)} {...props}>\n    <MoreHorizontal className=\"h-4 w-4\" />\n    <span className=\"sr-only\">More pages</span>\n  </span>\n);\nPaginationEllipsis.displayName = \"PaginationEllipsis\";\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationEllipsis,\n  PaginationItem,\n  PaginationLink,\n  PaginationNext,\n  PaginationPrevious,\n};\n"
    },
    {
      "name": "src/components/ui/popover.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent };\n"
    },
    {
      "name": "src/components/ui/progress.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Progress = React.forwardRef<\n  React.ElementRef<typeof ProgressPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>\n>(({ className, value, ...props }, ref) => (\n  <ProgressPrimitive.Root\n    ref={ref}\n    className={cn(\"relative h-4 w-full overflow-hidden rounded-full bg-secondary\", className)}\n    {...props}\n  >\n    <ProgressPrimitive.Indicator\n      className=\"h-full w-full flex-1 bg-primary transition-all\"\n      style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n    />\n  </ProgressPrimitive.Root>\n));\nProgress.displayName = ProgressPrimitive.Root.displayName;\n\nexport { Progress };\n"
    },
    {
      "name": "src/components/ui/radio-group.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { Circle } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst RadioGroup = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return <RadioGroupPrimitive.Root className={cn(\"grid gap-2\", className)} {...props} ref={ref} />;\n});\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n\nconst RadioGroupItem = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        \"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n        <Circle className=\"h-2.5 w-2.5 fill-current text-current\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n});\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\nexport { RadioGroup, RadioGroupItem };\n"
    },
    {
      "name": "src/components/ui/resizable.tsx",
      "type": "text/plain",
      "content": "import { GripVertical } from \"lucide-react\";\nimport * as ResizablePrimitive from \"react-resizable-panels\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (\n  <ResizablePrimitive.PanelGroup\n    className={cn(\"flex h-full w-full data-[panel-group-direction=vertical]:flex-col\", className)}\n    {...props}\n  />\n);\n\nconst ResizablePanel = ResizablePrimitive.Panel;\n\nconst ResizableHandle = ({\n  withHandle,\n  className,\n  ...props\n}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {\n  withHandle?: boolean;\n}) => (\n  <ResizablePrimitive.PanelResizeHandle\n    className={cn(\n      \"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-panel-group-direction=vertical]>div]:rotate-90\",\n      className,\n    )}\n    {...props}\n  >\n    {withHandle && (\n      <div className=\"z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border\">\n        <GripVertical className=\"h-2.5 w-2.5\" />\n      </div>\n    )}\n  </ResizablePrimitive.PanelResizeHandle>\n);\n\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle };\n"
    },
    {
      "name": "src/components/ui/scroll-area.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root ref={ref} className={cn(\"relative overflow-hidden\", className)} {...props}>\n    <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">{children}</ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n));\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" && \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" && \"h-2.5 flex-col border-t border-t-transparent p-[1px]\",\n      className,\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-border\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n));\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;\n\nexport { ScrollArea, ScrollBar };\n"
    },
    {
      "name": "src/components/ui/select.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\"flex cursor-pointer items-center justify-center py-1\", className)}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\"flex cursor-pointer items-center justify-center py-1\", className)}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className,\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label ref={ref} className={cn(\"py-1.5 pl-8 pr-2 text-sm font-semibold\", className)} {...props} />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator ref={ref} className={cn(\"-mx-1 my-1 h-px bg-muted\", className)} {...props} />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
    },
    {
      "name": "src/components/ui/separator.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(({ className, orientation = \"horizontal\", decorative = true, ...props }, ref) => (\n  <SeparatorPrimitive.Root\n    ref={ref}\n    decorative={decorative}\n    orientation={orientation}\n    className={cn(\"shrink-0 bg-border\", orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\", className)}\n    {...props}\n  />\n));\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
    },
    {
      "name": "src/components/ui/sheet.tsx",
      "type": "text/plain",
      "content": "import * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Sheet = SheetPrimitive.Root;\n\nconst SheetTrigger = SheetPrimitive.Trigger;\n\nconst SheetClose = SheetPrimitive.Close;\n\nconst SheetPortal = SheetPrimitive.Portal;\n\nconst SheetOverlay = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nSheetOverlay.displayName = SheetPrimitive.Overlay.displayName;\n\nconst sheetVariants = cva(\n  \"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n  {\n    variants: {\n      side: {\n        top: \"inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top\",\n        bottom:\n          \"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom\",\n        left: \"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm\",\n        right:\n          \"inset-y-0 right-0 h-full w-3/4  border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm\",\n      },\n    },\n    defaultVariants: {\n      side: \"right\",\n    },\n  },\n);\n\ninterface SheetContentProps\n  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,\n    VariantProps<typeof sheetVariants> {}\n\nconst SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(\n  ({ side = \"right\", className, children, ...props }, ref) => (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>\n        {children}\n        <SheetPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-secondary hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none\">\n          <X className=\"h-4 w-4\" />\n          <span className=\"sr-only\">Close</span>\n        </SheetPrimitive.Close>\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  ),\n);\nSheetContent.displayName = SheetPrimitive.Content.displayName;\n\nconst SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col space-y-2 text-center sm:text-left\", className)} {...props} />\n);\nSheetHeader.displayName = \"SheetHeader\";\n\nconst SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", className)} {...props} />\n);\nSheetFooter.displayName = \"SheetFooter\";\n\nconst SheetTitle = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Title ref={ref} className={cn(\"text-lg font-semibold text-foreground\", className)} {...props} />\n));\nSheetTitle.displayName = SheetPrimitive.Title.displayName;\n\nconst SheetDescription = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Description ref={ref} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n));\nSheetDescription.displayName = SheetPrimitive.Description.displayName;\n\nexport {\n  Sheet,\n  SheetClose,\n  SheetContent,\n  SheetDescription,\n  SheetFooter,\n  SheetHeader,\n  SheetOverlay,\n  SheetPortal,\n  SheetTitle,\n  SheetTrigger,\n};\n"
    },
    {
      "name": "src/components/ui/sidebar.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { VariantProps, cva } from \"class-variance-authority\";\nimport { PanelLeft } from \"lucide-react\";\n\nimport { useIsMobile } from \"@/hooks/use-mobile\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Sheet, SheetContent } from \"@/components/ui/sheet\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar:state\";\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;\nconst SIDEBAR_WIDTH_DEFAULT = \"16rem\";\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\";\nconst SIDEBAR_WIDTH_ICON = \"3rem\";\nconst SIDEBAR_WIDTH_MIN = 192; // 12rem\nconst SIDEBAR_WIDTH_MAX = 448; // 28rem\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\";\nconst SIDEBAR_WIDTH_STORAGE_KEY = \"sidebar:width\";\n\ntype SidebarContext = {\n  state: \"expanded\" | \"collapsed\";\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  openMobile: boolean;\n  setOpenMobile: (open: boolean) => void;\n  isMobile: boolean;\n  toggleSidebar: () => void;\n  sidebarWidth: string;\n};\n\nconst SidebarContext = React.createContext<SidebarContext | null>(null);\n\nfunction useSidebar() {\n  const context = React.useContext(SidebarContext);\n  if (!context) {\n    throw new Error(\"useSidebar must be used within a SidebarProvider.\");\n  }\n\n  return context;\n}\n\nconst SidebarProvider = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentProps<\"div\"> & {\n    defaultOpen?: boolean;\n    open?: boolean;\n    onOpenChange?: (open: boolean) => void;\n  }\n>(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => {\n  const isMobile = useIsMobile();\n  const [openMobile, setOpenMobile] = React.useState(false);\n\n  const [_open, _setOpen] = React.useState(defaultOpen);\n  const open = openProp ?? _open;\n  const setOpen = React.useCallback(\n    (value: boolean | ((value: boolean) => boolean)) => {\n      const openState = typeof value === \"function\" ? value(open) : value;\n      if (setOpenProp) {\n        setOpenProp(openState);\n      } else {\n        _setOpen(openState);\n      }\n      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;\n    },\n    [setOpenProp, open],\n  );\n\n  // Resizable width state\n  const [sidebarWidth, setSidebarWidth] = React.useState(() => {\n    try {\n      return localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY) || SIDEBAR_WIDTH_DEFAULT;\n    } catch {\n      return SIDEBAR_WIDTH_DEFAULT;\n    }\n  });\n\n  const toggleSidebar = React.useCallback(() => {\n    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);\n  }, [isMobile, setOpen, setOpenMobile]);\n\n  React.useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {\n        event.preventDefault();\n        toggleSidebar();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [toggleSidebar]);\n\n  const state = open ? \"expanded\" : \"collapsed\";\n\n  const contextValue = React.useMemo<SidebarContext>(\n    () => ({\n      state,\n      open,\n      setOpen,\n      isMobile,\n      openMobile,\n      setOpenMobile,\n      toggleSidebar,\n      sidebarWidth,\n    }),\n    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, sidebarWidth],\n  );\n\n  // Drag resize handler\n  const isDragging = React.useRef(false);\n\n  const handleMouseDown = React.useCallback((e: React.MouseEvent) => {\n    if (state === \"collapsed\") return;\n    e.preventDefault();\n    isDragging.current = true;\n    document.body.style.cursor = \"col-resize\";\n    document.body.style.userSelect = \"none\";\n\n    const onMouseMove = (ev: MouseEvent) => {\n      if (!isDragging.current) return;\n      const newWidth = Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, ev.clientX));\n      const widthStr = `${newWidth}px`;\n      setSidebarWidth(widthStr);\n      try { localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, widthStr); } catch { /* Storage may be unavailable in private mode. */ }\n    };\n\n    const onMouseUp = () => {\n      isDragging.current = false;\n      document.body.style.cursor = \"\";\n      document.body.style.userSelect = \"\";\n      document.removeEventListener(\"mousemove\", onMouseMove);\n      document.removeEventListener(\"mouseup\", onMouseUp);\n    };\n\n    document.addEventListener(\"mousemove\", onMouseMove);\n    document.addEventListener(\"mouseup\", onMouseUp);\n  }, [state]);\n\n  return (\n    <SidebarContext.Provider value={contextValue}>\n      <TooltipProvider delayDuration={0}>\n        <div\n          data-sidebar-drag-handler={state === \"expanded\" ? \"true\" : undefined}\n          style={\n            {\n              \"--sidebar-width\": sidebarWidth,\n              \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n              ...style,\n            } as React.CSSProperties\n          }\n          className={cn(\"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar\", className)}\n          ref={ref}\n          {...props}\n        >\n          {children}\n          {/* Drag resize handle, rendered at provider level to be sibling of sidebar */}\n          {state === \"expanded\" && !isMobile && (\n            <div\n              onMouseDown={handleMouseDown}\n              className=\"fixed inset-y-0 z-20 w-[5px] cursor-col-resize group/drag-handle\"\n              style={{ left: `calc(${sidebarWidth} - 2px)` }}\n            >\n              <div className=\"absolute inset-y-0 left-1/2 w-[2px] -translate-x-1/2 transition-colors duration-150 bg-transparent group-hover/drag-handle:bg-primary/40 group-active/drag-handle:bg-primary/60\" />\n            </div>\n          )}\n        </div>\n      </TooltipProvider>\n    </SidebarContext.Provider>\n  );\n});\nSidebarProvider.displayName = \"SidebarProvider\";\n\nconst Sidebar = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentProps<\"div\"> & {\n    side?: \"left\" | \"right\";\n    variant?: \"sidebar\" | \"floating\" | \"inset\";\n    collapsible?: \"offcanvas\" | \"icon\" | \"none\";\n  }\n>(({ side = \"left\", variant = \"sidebar\", collapsible = \"offcanvas\", className, children, ...props }, ref) => {\n  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();\n\n  if (collapsible === \"none\") {\n    return (\n      <div\n        className={cn(\"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground\", className)}\n        ref={ref}\n        {...props}\n      >\n        {children}\n      </div>\n    );\n  }\n\n  if (isMobile) {\n    return (\n      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>\n        <SheetContent\n          data-sidebar=\"sidebar\"\n          data-mobile=\"true\"\n          className=\"w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden\"\n          style={\n            {\n              \"--sidebar-width\": SIDEBAR_WIDTH_MOBILE,\n            } as React.CSSProperties\n          }\n          side={side}\n        >\n          <div className=\"flex h-full w-full flex-col\">{children}</div>\n        </SheetContent>\n      </Sheet>\n    );\n  }\n\n  return (\n    <div\n      ref={ref}\n      className=\"group peer hidden text-sidebar-foreground lg:block\"\n      data-state={state}\n      data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n      data-variant={variant}\n      data-side={side}\n    >\n      {/* This is what handles the sidebar gap on desktop */}\n      <div\n        className={cn(\n          \"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]\",\n          \"group-data-[collapsible=offcanvas]:w-0\",\n          \"group-data-[side=right]:rotate-180\",\n          variant === \"floating\" || variant === \"inset\"\n            ? \"group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]\"\n            : \"group-data-[collapsible=icon]:w-[--sidebar-width-icon]\",\n        )}\n      />\n      <div\n        className={cn(\n          \"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] lg:flex\",\n          side === \"left\"\n            ? \"left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]\"\n            : \"right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]\",\n          variant === \"floating\" || variant === \"inset\"\n            ? \"p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]\"\n            : \"group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l\",\n          className,\n        )}\n        {...props}\n      >\n        <div\n          data-sidebar=\"sidebar\"\n          className=\"flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow\"\n        >\n          {children}\n        </div>\n      </div>\n    </div>\n  );\n});\nSidebar.displayName = \"Sidebar\";\n\nconst SidebarTrigger = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(\n  ({ className, onClick, ...props }, ref) => {\n    const { toggleSidebar } = useSidebar();\n\n    return (\n      <Button\n        ref={ref}\n        data-sidebar=\"trigger\"\n        variant=\"ghost\"\n        size=\"icon\"\n        className={cn(\"h-7 w-7\", className)}\n        onClick={(event) => {\n          onClick?.(event);\n          toggleSidebar();\n        }}\n        {...props}\n      >\n        <PanelLeft />\n        <span className=\"sr-only\">Toggle Sidebar</span>\n      </Button>\n    );\n  },\n);\nSidebarTrigger.displayName = \"SidebarTrigger\";\n\nconst SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\">>(\n  ({ className, ...props }, ref) => {\n    const { toggleSidebar } = useSidebar();\n\n    return (\n      <button\n        ref={ref}\n        data-sidebar=\"rail\"\n        aria-label=\"Toggle Sidebar\"\n        tabIndex={-1}\n        onClick={toggleSidebar}\n        title=\"Toggle Sidebar\"\n        className={cn(\n          \"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 hover:after:bg-sidebar-border sm:flex\",\n          \"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize\",\n          \"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize\",\n          \"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar\",\n          \"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2\",\n          \"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nSidebarRail.displayName = \"SidebarRail\";\n\nconst SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<\"main\">>(({ className, ...props }, ref) => {\n  return (\n    <main\n      ref={ref}\n      className={cn(\n        \"relative flex min-h-svh flex-1 flex-col bg-background\",\n        \"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] lg:peer-data-[variant=inset]:m-2 lg:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 lg:peer-data-[variant=inset]:ml-0 lg:peer-data-[variant=inset]:rounded-xl lg:peer-data-[variant=inset]:shadow\",\n        className,\n      )}\n      {...props}\n    />\n  );\n});\nSidebarInset.displayName = \"SidebarInset\";\n\nconst SidebarInput = React.forwardRef<React.ElementRef<typeof Input>, React.ComponentProps<typeof Input>>(\n  ({ className, ...props }, ref) => {\n    return (\n      <Input\n        ref={ref}\n        data-sidebar=\"input\"\n        className={cn(\n          \"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nSidebarInput.displayName = \"SidebarInput\";\n\nconst SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(({ className, ...props }, ref) => {\n  return <div ref={ref} data-sidebar=\"header\" className={cn(\"flex flex-col gap-2 p-2\", className)} {...props} />;\n});\nSidebarHeader.displayName = \"SidebarHeader\";\n\nconst SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(({ className, ...props }, ref) => {\n  return <div ref={ref} data-sidebar=\"footer\" className={cn(\"flex flex-col gap-2 p-2\", className)} {...props} />;\n});\nSidebarFooter.displayName = \"SidebarFooter\";\n\nconst SidebarSeparator = React.forwardRef<React.ElementRef<typeof Separator>, React.ComponentProps<typeof Separator>>(\n  ({ className, ...props }, ref) => {\n    return (\n      <Separator\n        ref={ref}\n        data-sidebar=\"separator\"\n        className={cn(\"mx-2 w-auto bg-sidebar-border\", className)}\n        {...props}\n      />\n    );\n  },\n);\nSidebarSeparator.displayName = \"SidebarSeparator\";\n\nconst SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(({ className, ...props }, ref) => {\n  return (\n    <div\n      ref={ref}\n      data-sidebar=\"content\"\n      className={cn(\n        \"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden scrollbar-hide\",\n        className,\n      )}\n      style={{ scrollbarWidth: \"none\", msOverflowStyle: \"none\" }}\n      {...props}\n    />\n  );\n});\nSidebarContent.displayName = \"SidebarContent\";\n\nconst SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(({ className, ...props }, ref) => {\n  return (\n    <div\n      ref={ref}\n      data-sidebar=\"group\"\n      className={cn(\"relative flex w-full min-w-0 flex-col p-2\", className)}\n      {...props}\n    />\n  );\n});\nSidebarGroup.displayName = \"SidebarGroup\";\n\nconst SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\"> & { asChild?: boolean }>(\n  ({ className, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"div\";\n\n    return (\n      <Comp\n        ref={ref}\n        data-sidebar=\"group-label\"\n        className={cn(\n          \"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n          \"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nSidebarGroupLabel.displayName = \"SidebarGroupLabel\";\n\nconst SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\"> & { asChild?: boolean }>(\n  ({ className, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n\n    return (\n      <Comp\n        ref={ref}\n        data-sidebar=\"group-action\"\n        className={cn(\n          \"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n          \"after:absolute after:-inset-2 after:lg:hidden\",\n          \"group-data-[collapsible=icon]:hidden\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nSidebarGroupAction.displayName = \"SidebarGroupAction\";\n\nconst SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} data-sidebar=\"group-content\" className={cn(\"w-full text-sm\", className)} {...props} />\n  ),\n);\nSidebarGroupContent.displayName = \"SidebarGroupContent\";\n\nconst SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(({ className, ...props }, ref) => (\n  <ul ref={ref} data-sidebar=\"menu\" className={cn(\"flex w-full min-w-0 flex-col gap-1\", className)} {...props} />\n));\nSidebarMenu.displayName = \"SidebarMenu\";\n\nconst SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(({ className, ...props }, ref) => (\n  <li ref={ref} data-sidebar=\"menu-item\" className={cn(\"group/menu-item relative\", className)} {...props} />\n));\nSidebarMenuItem.displayName = \"SidebarMenuItem\";\n\nconst sidebarMenuButtonVariants = cva(\n  \"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n        outline:\n          \"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]\",\n      },\n      size: {\n        default: \"h-8 text-sm\",\n        sm: \"h-7 text-xs\",\n        lg: \"h-12 text-sm group-data-[collapsible=icon]:!p-0\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nconst SidebarMenuButton = React.forwardRef<\n  HTMLButtonElement,\n  React.ComponentProps<\"button\"> & {\n    asChild?: boolean;\n    isActive?: boolean;\n    tooltip?: string | React.ComponentProps<typeof TooltipContent>;\n  } & VariantProps<typeof sidebarMenuButtonVariants>\n>(({ asChild = false, isActive = false, variant = \"default\", size = \"default\", tooltip, className, ...props }, ref) => {\n  const Comp = asChild ? Slot : \"button\";\n  const { isMobile, state } = useSidebar();\n\n  const button = (\n    <Comp\n      ref={ref}\n      data-sidebar=\"menu-button\"\n      data-size={size}\n      data-active={isActive}\n      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n      {...props}\n    />\n  );\n\n  if (!tooltip) {\n    return button;\n  }\n\n  if (typeof tooltip === \"string\") {\n    tooltip = {\n      children: tooltip,\n    };\n  }\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>{button}</TooltipTrigger>\n      <TooltipContent side=\"right\" align=\"center\" hidden={state !== \"collapsed\" || isMobile} {...tooltip} />\n    </Tooltip>\n  );\n});\nSidebarMenuButton.displayName = \"SidebarMenuButton\";\n\nconst SidebarMenuAction = React.forwardRef<\n  HTMLButtonElement,\n  React.ComponentProps<\"button\"> & {\n    asChild?: boolean;\n    showOnHover?: boolean;\n  }\n>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {\n  const Comp = asChild ? Slot : \"button\";\n\n  return (\n    <Comp\n      ref={ref}\n      data-sidebar=\"menu-action\"\n      className={cn(\n        \"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n        \"after:absolute after:-inset-2 after:lg:hidden\",\n        \"peer-data-[size=sm]/menu-button:top-1\",\n        \"peer-data-[size=default]/menu-button:top-1.5\",\n        \"peer-data-[size=lg]/menu-button:top-2.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        showOnHover &&\n          \"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground lg:opacity-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n});\nSidebarMenuAction.displayName = \"SidebarMenuAction\";\n\nconst SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n  ({ className, ...props }, ref) => (\n    <div\n      ref={ref}\n      data-sidebar=\"menu-badge\"\n      className={cn(\n        \"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground\",\n        \"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground\",\n        \"peer-data-[size=sm]/menu-button:top-1\",\n        \"peer-data-[size=default]/menu-button:top-1.5\",\n        \"peer-data-[size=lg]/menu-button:top-2.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nSidebarMenuBadge.displayName = \"SidebarMenuBadge\";\n\nconst SidebarMenuSkeleton = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentProps<\"div\"> & {\n    showIcon?: boolean;\n  }\n>(({ className, showIcon = false, ...props }, ref) => {\n  const width = React.useMemo(() => {\n    return `${Math.floor(Math.random() * 40) + 50}%`;\n  }, []);\n\n  return (\n    <div\n      ref={ref}\n      data-sidebar=\"menu-skeleton\"\n      className={cn(\"flex h-8 items-center gap-2 rounded-md px-2\", className)}\n      {...props}\n    >\n      {showIcon && <Skeleton className=\"size-4 rounded-md\" data-sidebar=\"menu-skeleton-icon\" />}\n      <Skeleton\n        className=\"h-4 max-w-[--skeleton-width] flex-1\"\n        data-sidebar=\"menu-skeleton-text\"\n        style={\n          {\n            \"--skeleton-width\": width,\n          } as React.CSSProperties\n        }\n      />\n    </div>\n  );\n});\nSidebarMenuSkeleton.displayName = \"SidebarMenuSkeleton\";\n\nconst SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(\n  ({ className, ...props }, ref) => (\n    <ul\n      ref={ref}\n      data-sidebar=\"menu-sub\"\n      className={cn(\n        \"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nSidebarMenuSub.displayName = \"SidebarMenuSub\";\n\nconst SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(({ ...props }, ref) => (\n  <li ref={ref} {...props} />\n));\nSidebarMenuSubItem.displayName = \"SidebarMenuSubItem\";\n\nconst SidebarMenuSubButton = React.forwardRef<\n  HTMLAnchorElement,\n  React.ComponentProps<\"a\"> & {\n    asChild?: boolean;\n    size?: \"sm\" | \"md\";\n    isActive?: boolean;\n  }\n>(({ asChild = false, size = \"md\", isActive, className, ...props }, ref) => {\n  const Comp = asChild ? Slot : \"a\";\n\n  return (\n    <Comp\n      ref={ref}\n      data-sidebar=\"menu-sub-button\"\n      data-size={size}\n      data-active={isActive}\n      className={cn(\n        \"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground\",\n        \"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground\",\n        size === \"sm\" && \"text-xs\",\n        size === \"md\" && \"text-sm\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n});\nSidebarMenuSubButton.displayName = \"SidebarMenuSubButton\";\n\nexport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupAction,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarInput,\n  SidebarInset,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuBadge,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarMenuSkeleton,\n  SidebarMenuSub,\n  SidebarMenuSubButton,\n  SidebarMenuSubItem,\n  SidebarProvider,\n  SidebarRail,\n  SidebarSeparator,\n  SidebarTrigger,\n  useSidebar,\n};\n"
    },
    {
      "name": "src/components/ui/skeleton.tsx",
      "type": "text/plain",
      "content": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"animate-pulse rounded-md bg-muted\", className)} {...props} />;\n}\n\nexport { Skeleton };\n"
    },
    {
      "name": "src/components/ui/slider.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Slider = React.forwardRef<\n  React.ElementRef<typeof SliderPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <SliderPrimitive.Root\n    ref={ref}\n    className={cn(\"relative flex w-full touch-none select-none items-center\", className)}\n    {...props}\n  >\n    <SliderPrimitive.Track className=\"relative h-2 w-full grow overflow-hidden rounded-full bg-secondary\">\n      <SliderPrimitive.Range className=\"absolute h-full bg-primary\" />\n    </SliderPrimitive.Track>\n    <SliderPrimitive.Thumb className=\"block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\" />\n  </SliderPrimitive.Root>\n));\nSlider.displayName = SliderPrimitive.Root.displayName;\n\nexport { Slider };\n"
    },
    {
      "name": "src/components/ui/sonner.tsx",
      "type": "text/plain",
      "content": "import { useTheme } from \"next-themes\";\nimport { Toaster as Sonner, toast } from \"sonner\";\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>;\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme();\n\n  return (\n    <Sonner\n      theme={theme as ToasterProps[\"theme\"]}\n      className=\"toaster group\"\n      toastOptions={{\n        classNames: {\n          toast:\n            \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n          description: \"group-[.toast]:text-muted-foreground\",\n          actionButton: \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n          cancelButton: \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n        },\n      }}\n      {...props}\n    />\n  );\n};\n\nexport { Toaster, toast };\n"
    },
    {
      "name": "src/components/ui/switch.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0\",\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
    },
    {
      "name": "src/components/ui/table.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(\n  ({ className, ...props }, ref) => (\n    <div className=\"relative w-full overflow-auto\">\n      <table ref={ref} className={cn(\"w-full caption-bottom text-sm\", className)} {...props} />\n    </div>\n  ),\n);\nTable.displayName = \"Table\";\n\nconst TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(\n  ({ className, ...props }, ref) => <thead ref={ref} className={cn(\"[&_tr]:border-b\", className)} {...props} />,\n);\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(\n  ({ className, ...props }, ref) => (\n    <tbody ref={ref} className={cn(\"[&_tr:last-child]:border-0\", className)} {...props} />\n  ),\n);\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(\n  ({ className, ...props }, ref) => (\n    <tfoot ref={ref} className={cn(\"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0\", className)} {...props} />\n  ),\n);\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(\n  ({ className, ...props }, ref) => (\n    <tr\n      ref={ref}\n      className={cn(\"border-b transition-colors data-[state=selected]:bg-muted hover:bg-muted/50\", className)}\n      {...props}\n    />\n  ),\n);\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(\n  ({ className, ...props }, ref) => (\n    <th\n      ref={ref}\n      className={cn(\n        \"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(\n  ({ className, ...props }, ref) => (\n    <td ref={ref} className={cn(\"p-4 align-middle [&:has([role=checkbox])]:pr-0\", className)} {...props} />\n  ),\n);\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(\n  ({ className, ...props }, ref) => (\n    <caption ref={ref} className={cn(\"mt-4 text-sm text-muted-foreground\", className)} {...props} />\n  ),\n);\nTableCaption.displayName = \"TableCaption\";\n\nexport { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };\n"
    },
    {
      "name": "src/components/ui/tabs.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Tabs = TabsPrimitive.Root;\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      \"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
    },
    {
      "name": "src/components/ui/textarea.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {\n  return (\n    <textarea\n      className={cn(\n        \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    />\n  );\n});\nTextarea.displayName = \"Textarea\";\n\nexport { Textarea };\n"
    },
    {
      "name": "src/components/ui/toast.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst ToastProvider = ToastPrimitives.Provider;\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n      className,\n    )}\n    {...props}\n  />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n  {\n    variants: {\n      variant: {\n        default: \"border bg-background text-foreground\",\n        destructive: \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />;\n});\nToast.displayName = ToastPrimitives.Root.displayName;\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors group-[.destructive]:border-muted/40 hover:bg-secondary group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 group-[.destructive]:focus-visible:ring-destructive disabled:pointer-events-none disabled:opacity-50\",\n      className,\n    )}\n    {...props}\n  />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      \"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 hover:text-foreground group-[.destructive]:hover:text-red-50 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 group-[.destructive]:focus-visible:ring-red-400 group-[.destructive]:focus-visible:ring-offset-red-600\",\n      className,\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <X className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title ref={ref} className={cn(\"text-sm font-semibold\", className)} {...props} />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description ref={ref} className={cn(\"text-sm opacity-90\", className)} {...props} />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>;\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n};\n"
    },
    {
      "name": "src/components/ui/toaster.tsx",
      "type": "text/plain",
      "content": "import { useToast } from \"@/hooks/use-toast\";\nimport { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from \"@/components/ui/toast\";\n\nexport function Toaster() {\n  const { toasts } = useToast();\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && <ToastDescription>{description}</ToastDescription>}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        );\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  );\n}\n"
    },
    {
      "name": "src/components/ui/toggle-group.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\";\nimport { type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\nimport { toggleVariants } from \"@/components/ui/toggle\";\n\nconst ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({\n  size: \"default\",\n  variant: \"default\",\n});\n\nconst ToggleGroup = React.forwardRef<\n  React.ElementRef<typeof ToggleGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants>\n>(({ className, variant, size, children, ...props }, ref) => (\n  <ToggleGroupPrimitive.Root ref={ref} className={cn(\"flex items-center justify-center gap-1\", className)} {...props}>\n    <ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>\n  </ToggleGroupPrimitive.Root>\n));\n\nToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;\n\nconst ToggleGroupItem = React.forwardRef<\n  React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>\n>(({ className, children, variant, size, ...props }, ref) => {\n  const context = React.useContext(ToggleGroupContext);\n\n  return (\n    <ToggleGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        toggleVariants({\n          variant: context.variant || variant,\n          size: context.size || size,\n        }),\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </ToggleGroupPrimitive.Item>\n  );\n});\n\nToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;\n\nexport { ToggleGroup, ToggleGroupItem };\n"
    },
    {
      "name": "src/components/ui/toggle.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline: \"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-10 px-3\",\n        sm: \"h-9 px-2.5\",\n        lg: \"h-11 px-5\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nconst Toggle = React.forwardRef<\n  React.ElementRef<typeof TogglePrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>\n>(({ className, variant, size, ...props }, ref) => (\n  <TogglePrimitive.Root ref={ref} className={cn(toggleVariants({ variant, size, className }))} {...props} />\n));\n\nToggle.displayName = TogglePrimitive.Root.displayName;\n\nexport { Toggle, toggleVariants };\n"
    },
    {
      "name": "src/components/ui/tooltip.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst Tooltip = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
    },
    {
      "name": "src/components/ui/use-toast.ts",
      "type": "text/plain",
      "content": "import { useToast, toast } from \"@/hooks/use-toast\";\n\nexport { useToast, toast };\n"
    },
    {
      "name": "src/hooks/use-mobile.tsx",
      "type": "text/plain",
      "content": "import * as React from \"react\";\n\n// The sidebar becomes an off-canvas drawer below this width (Tailwind `lg`).\n// At tablet widths a pinned 256px sidebar left too little room for the content.\nconst MOBILE_BREAKPOINT = 1024;\n\nexport function useIsMobile() {\n  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);\n\n  React.useEffect(() => {\n    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n    const onChange = () => {\n      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n    };\n    mql.addEventListener(\"change\", onChange);\n    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n    return () => mql.removeEventListener(\"change\", onChange);\n  }, []);\n\n  return !!isMobile;\n}\n"
    },
    {
      "name": "src/hooks/use-toast.ts",
      "type": "text/plain",
      "content": "import * as React from \"react\";\n\nimport type { ToastActionElement, ToastProps } from \"@/components/ui/toast\";\n\nconst TOAST_LIMIT = 1;\nconst TOAST_REMOVE_DELAY = 1000000;\n\ntype ToasterToast = ToastProps & {\n  id: string;\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  action?: ToastActionElement;\n};\n\nconst actionTypes = {\n  ADD_TOAST: \"ADD_TOAST\",\n  UPDATE_TOAST: \"UPDATE_TOAST\",\n  DISMISS_TOAST: \"DISMISS_TOAST\",\n  REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const;\n\nlet count = 0;\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER;\n  return count.toString();\n}\n\ntype ActionType = typeof actionTypes;\n\ntype Action =\n  | {\n      type: ActionType[\"ADD_TOAST\"];\n      toast: ToasterToast;\n    }\n  | {\n      type: ActionType[\"UPDATE_TOAST\"];\n      toast: Partial<ToasterToast>;\n    }\n  | {\n      type: ActionType[\"DISMISS_TOAST\"];\n      toastId?: ToasterToast[\"id\"];\n    }\n  | {\n      type: ActionType[\"REMOVE_TOAST\"];\n      toastId?: ToasterToast[\"id\"];\n    };\n\ninterface State {\n  toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return;\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId);\n    dispatch({\n      type: \"REMOVE_TOAST\",\n      toastId: toastId,\n    });\n  }, TOAST_REMOVE_DELAY);\n\n  toastTimeouts.set(toastId, timeout);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case \"ADD_TOAST\":\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      };\n\n    case \"UPDATE_TOAST\":\n      return {\n        ...state,\n        toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),\n      };\n\n    case \"DISMISS_TOAST\": {\n      const { toastId } = action;\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId);\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id);\n        });\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t,\n        ),\n      };\n    }\n    case \"REMOVE_TOAST\":\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        };\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      };\n  }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action);\n  listeners.forEach((listener) => {\n    listener(memoryState);\n  });\n}\n\ntype Toast = Omit<ToasterToast, \"id\">;\n\nfunction toast({ ...props }: Toast) {\n  const id = genId();\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: \"UPDATE_TOAST\",\n      toast: { ...props, id },\n    });\n  const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id });\n\n  dispatch({\n    type: \"ADD_TOAST\",\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss();\n      },\n    },\n  });\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  };\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState);\n\n  React.useEffect(() => {\n    listeners.push(setState);\n    return () => {\n      const index = listeners.indexOf(setState);\n      if (index > -1) {\n        listeners.splice(index, 1);\n      }\n    };\n  }, [state]);\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n  };\n}\n\nexport { useToast, toast };\n"
    },
    {
      "name": "src/lib/calendarActivity.ts",
      "type": "text/plain",
      "content": "import type { Transaction } from \"@/types/finance\";\n\nexport interface CalendarSelection { from: string; to: string }\nexport interface ActivityDay { date: string; inflow: number; outflow: number; count: number }\nexport type FlowDirection = \"outflow\" | \"inflow\";\n\n/** Read a calendar date without UTC/local conversion or JavaScript's date rollover. */\nexport function calendarDate(value: string | null | undefined): string | null {\n  if (!value || !/^\\d{4}-\\d{2}-\\d{2}(?:$|T)/.test(value)) return null;\n  const key = value.slice(0, 10);\n  const date = new Date(`${key}T00:00:00Z`);\n  return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === key ? key : null;\n}\n\nexport function dailyCashflow(transactions: Transaction[]): Map<string, ActivityDay> {\n  const days = new Map<string, ActivityDay>();\n  for (const t of transactions) {\n    const date = calendarDate(t.date);\n    if (!date || !Number.isFinite(t.amount)) continue;\n    const day = days.get(date) ?? { date, inflow: 0, outflow: 0, count: 0 };\n    // Accumulate integer cents; direction follows the type, even on imports\n    // whose expense amount is positive. Transfers are cash movement here.\n    day[t.type === \"income\" ? \"inflow\" : \"outflow\"] += Math.round(Math.abs(t.amount) * 100);\n    day.count += 1;\n    days.set(date, day);\n  }\n  for (const day of days.values()) { day.inflow /= 100; day.outflow /= 100; }\n  return days;\n}\n\nexport function orderedSelection(a: string, b: string): CalendarSelection {\n  return a <= b ? { from: a, to: b } : { from: b, to: a };\n}\n\nexport function inCalendarSelection(date: string, selection: CalendarSelection): boolean {\n  return date >= selection.from && date <= selection.to;\n}\n\n/** Sunday-first, column-major weeks. Padding cells are not dates/buttons. */\nexport function calendarWeeks(year: number): (string | null)[][] {\n  const first = new Date(Date.UTC(year, 0, 1));\n  const last = new Date(Date.UTC(year, 11, 31));\n  const count = Math.round((last.getTime() - first.getTime()) / 86_400_000) + 1;\n  const size = Math.ceil((first.getUTCDay() + count) / 7) * 7;\n  const cells = Array.from({ length: size }, (_, i) => {\n    const date = new Date(Date.UTC(year, 0, 1 + i - first.getUTCDay()));\n    return date.getUTCFullYear() === year ? date.toISOString().slice(0, 10) : null;\n  });\n  return Array.from({ length: size / 7 }, (_, i) => cells.slice(i * 7, i * 7 + 7));\n}\n\n/** Rank-based levels keep a large bill from washing out every everyday purchase. */\nexport function activityThresholds(days: ReadonlyMap<string, ActivityDay>, direction: FlowDirection): number[] {\n  const values = [...days.values()].map(d => d[direction]).filter(n => n > 0).sort((a, b) => a - b);\n  return values.length ? [0.25, 0.5, 0.75].map(p => values[Math.floor((values.length - 1) * p)]) : [];\n}\n\nexport function activityLevel(amount: number, thresholds: number[]): number {\n  if (amount <= 0 || !thresholds.length) return 0;\n  return 1 + thresholds.filter(n => amount > n).length;\n}\n"
    },
    {
      "name": "src/lib/utils.ts",
      "type": "text/plain",
      "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\nexport function parseLocalDate(dateStr: string): Date {\n  if (!dateStr) return new Date();\n  // Take the calendar date from \"YYYY-MM-DD\" and from datetimes that start\n  // with it (\"2026-09-20T00:00:00\"), which some imports store; splitting the\n  // whole string on \"-\" turned the day into NaN and the date into Invalid Date.\n  const m = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(dateStr);\n  if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n  return new Date(dateStr);\n}\n"
    }
  ]
}
