{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "demo-sandbox-panel",
  "title": "Demo — Sandbox Panel",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/demo-sandbox-panel/demo-sandbox-panel.component.ts",
      "content": "// Interactive sandbox panel where editing a definition field updates a rendered result\n// and a generated query side by side, with a reset control. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport { RotateCcw, LucideAngularModule } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport {\n  UiTabsComponent,\n  UiTabsContentComponent,\n  UiTabsListComponent,\n  UiTabsTriggerComponent,\n} from '@/ui/tabs/tabs.component'\n\nexport interface SandboxForm {\n  metric: string\n  window: string\n  groupBy: string\n}\n\nexport interface SandboxResultRow {\n  key: string\n  value: number\n}\n\nexport const SANDBOX_DEFAULTS: SandboxForm = { metric: 'revenue_net', window: 'trailing_28d', groupBy: 'region' }\n\nexport const SANDBOX_BASE: Record<string, { key: string; value: number }[]> = {\n  region: [\n    { key: 'EMEA', value: 1_842_100 },\n    { key: 'AMER', value: 1_536_400 },\n    { key: 'APAC', value: 762_400 },\n  ],\n  channel: [\n    { key: 'Direct', value: 2_104_900 },\n    { key: 'Marketplace', value: 1_218_600 },\n    { key: 'Wholesale', value: 817_400 },\n  ],\n}\n\nexport const SANDBOX_MULTIPLIER: Record<string, number> = {\n  trailing_7d: 0.25,\n  trailing_28d: 1,\n  trailing_90d: 3.1,\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-demo-sandbox-panel, [ui-demo-sandbox-panel]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"demo-sandbox-panel\"',\n    '[class]': 'hostClass',\n  },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiInputComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n    UiSeparatorComponent,\n    UiTabsComponent,\n    UiTabsContentComponent,\n    UiTabsListComponent,\n    UiTabsTriggerComponent,\n  ],\n  template: `\n    <div class=\"mx-auto max-w-5xl px-6 py-20 lg:py-28\">\n      <div class=\"flex flex-wrap items-end justify-between gap-6\">\n        <div class=\"max-w-xl\">\n          <span ui-badge variant=\"secondary\">Sandbox</span>\n          <h2 class=\"mt-4 text-3xl font-semibold tracking-tight sm:text-4xl\">\n            Change the definition, watch both sides\n          </h2>\n        </div>\n        <button ui-button variant=\"ghost\" size=\"sm\" (click)=\"reset()\">\n          <lucide-icon [img]=\"RotateCcw\" class=\"mr-1.5 size-3.5\" />\n          Reset\n        </button>\n      </div>\n\n      <div ui-card class=\"mt-8\">\n        <div ui-card-content class=\"grid gap-6 p-6 lg:grid-cols-[18rem_1fr] lg:gap-8\">\n          <div class=\"space-y-4\">\n            <div>\n              <label for=\"sandbox-metric\" class=\"text-sm font-medium\">Metric</label>\n              <ui-input\n                id=\"sandbox-metric\"\n                [value]=\"metric()\"\n                (valueChange)=\"metric.set($event)\"\n                class=\"mt-1.5 font-mono text-sm\"\n              />\n            </div>\n\n            <div>\n              <label for=\"sandbox-window\" class=\"text-sm font-medium\">Window</label>\n              <ui-select [value]=\"window()\" (valueChange)=\"setWindow($event)\">\n                <button ui-select-trigger id=\"sandbox-window\" class=\"mt-1.5\">\n                  <ui-select-value />\n                </button>\n                <ui-select-content>\n                  <ui-select-item value=\"trailing_7d\">trailing_7d</ui-select-item>\n                  <ui-select-item value=\"trailing_28d\">trailing_28d</ui-select-item>\n                  <ui-select-item value=\"trailing_90d\">trailing_90d</ui-select-item>\n                </ui-select-content>\n              </ui-select>\n            </div>\n\n            <div>\n              <label for=\"sandbox-group\" class=\"text-sm font-medium\">Group by</label>\n              <ui-select [value]=\"groupBy()\" (valueChange)=\"setGroupBy($event)\">\n                <button ui-select-trigger id=\"sandbox-group\" class=\"mt-1.5\">\n                  <ui-select-value />\n                </button>\n                <ui-select-content>\n                  <ui-select-item value=\"region\">region</ui-select-item>\n                  <ui-select-item value=\"channel\">channel</ui-select-item>\n                </ui-select-content>\n              </ui-select>\n            </div>\n\n            <ui-separator />\n            <p class=\"text-muted-foreground text-xs leading-relaxed\">\n              Both panels recompute from these three fields. Nothing is cached between them.\n            </p>\n          </div>\n\n          <ui-tabs defaultValue=\"result\">\n            <ui-tabs-list variant=\"segmented\">\n              <button ui-tabs-trigger value=\"result\" variant=\"segmented\">Result</button>\n              <button ui-tabs-trigger value=\"query\" variant=\"segmented\">Generated query</button>\n            </ui-tabs-list>\n\n            <ui-tabs-content value=\"result\" class=\"mt-4\">\n              <div class=\"border-border min-h-[12rem] overflow-hidden rounded-lg border\">\n                <div class=\"border-border text-muted-foreground border-b px-4 py-2 font-mono text-xs\">\n                  {{ metric() }} · {{ window() }}\n                </div>\n                <ul class=\"divide-border divide-y\">\n                  @for (row of results(); track row.key) {\n                    <li class=\"flex items-center justify-between gap-4 px-4 py-2.5\">\n                      <span class=\"text-sm\">{{ row.key }}</span>\n                      <span class=\"font-mono text-sm tabular-nums\">£{{ formatValue(row.value) }}</span>\n                    </li>\n                  }\n                </ul>\n              </div>\n            </ui-tabs-content>\n\n            <ui-tabs-content value=\"query\" class=\"mt-4\">\n              <pre\n                class=\"border-border min-h-[12rem] overflow-x-auto rounded-lg border p-4 font-mono text-xs leading-relaxed\"\n              ><code>{{ query() }}</code></pre>\n            </ui-tabs-content>\n          </ui-tabs>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiDemoSandboxPanelComponent {\n  protected readonly RotateCcw = RotateCcw\n\n  @Input('class') className?: string\n\n  readonly metric = signal(SANDBOX_DEFAULTS.metric)\n  readonly window = signal(SANDBOX_DEFAULTS.window)\n  readonly groupBy = signal(SANDBOX_DEFAULTS.groupBy)\n\n  // Both outputs derive from the same form, so the query and the result can never\n  // describe different things — the failure mode of most fake sandboxes.\n  readonly query = computed(\n    () => `SELECT ${this.groupBy()},\n       SUM(amount) AS ${this.metric()}\nFROM   metrics.${this.metric()}\nWHERE  window = '${this.window()}'\nGROUP  BY ${this.groupBy()}\nORDER  BY ${this.metric()} DESC`,\n  )\n\n  readonly results = computed<SandboxResultRow[]>(() =>\n    (SANDBOX_BASE[this.groupBy()] ?? SANDBOX_BASE['region']!).map((row) => ({\n      key: row.key,\n      value: Math.round(row.value * (SANDBOX_MULTIPLIER[this.window()] ?? 1)),\n    })),\n  )\n\n  get hostClass(): string {\n    return cn('bg-background block w-full', this.className)\n  }\n\n  setWindow(value: string): void {\n    this.window.set(value)\n  }\n\n  setGroupBy(value: string): void {\n    this.groupBy.set(value)\n  }\n\n  formatValue(value: number): string {\n    return value.toLocaleString('en-GB')\n  }\n\n  reset(): void {\n    this.metric.set(SANDBOX_DEFAULTS.metric)\n    this.window.set(SANDBOX_DEFAULTS.window)\n    this.groupBy.set(SANDBOX_DEFAULTS.groupBy)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/demo-sandbox-panel/demo-sandbox-panel.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/demo-sandbox-panel/index.ts",
      "content": "export {\n  UiDemoSandboxPanelComponent,\n  SANDBOX_DEFAULTS,\n  SANDBOX_BASE,\n  SANDBOX_MULTIPLIER,\n  type SandboxForm,\n  type SandboxResultRow,\n} from './demo-sandbox-panel.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/demo-sandbox-panel/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/select.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/tabs.json"
  ],
  "description": "Interactive sandbox panel where editing a definition field updates a rendered result and a generated query side by side, with a reset control.",
  "categories": [
    "marketing"
  ]
}