# Scroll Panel

Use `ui-scroll-panel` when you need a reusable scroll shell for custom layouts, panels, drawers, or card regions and do not want the higher-level data-loading behavior of `ui-scroll-container`.

## Import
```ts
import { ScrollPanelComponent } from 'ui';
```

## Basic scroll region
```ts
import { Component } from '@angular/core';
import { ScrollPanelComponent } from 'ui';

@Component({
  selector: 'app-scroll-panel-basic-demo',
  standalone: true,
  imports: [ScrollPanelComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:28rem">
      <div
        style="border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest);padding:0.5rem"
      >
        <ui-scroll-panel maxHeight="22rem" ariaLabel="Activity feed panel">
          <div style="display:flex;flex-direction:column;gap:0.75rem">
            @for (item of items; track item.id) {
              <div
                style="padding:0.875rem 1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
              >
                <div style="font-size:0.875rem;font-weight:600">{{ item.title }}</div>
                <div
                  style="margin-top:0.25rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
                >
                  {{ item.body }}
                </div>
              </div>
            }
          </div>
        </ui-scroll-panel>
      </div>
    </div>
  `,
})
export class ScrollPanelBasicDemoComponent {
  protected readonly items = Array.from({ length: 14 }, (_, index) => ({
    id: index + 1,
    title: `Update ${index + 1}`,
    body: 'A basic vertical scroll region is useful for feeds, drawers, and side panels with constrained height.',
  }));
}
```

## Horizontal and bidirectional layouts
```ts
import { Component } from '@angular/core';
import { ScrollPanelComponent } from 'ui';

@Component({
  selector: 'app-scroll-panel-orientation-demo',
  standalone: true,
  imports: [ScrollPanelComponent],
  template: `
    <div
      style="display:grid;grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));gap:1rem;width:100%;max-width:54rem"
    >
      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.875rem;font-weight:600">Horizontal strip</div>
        <ui-scroll-panel
          orientation="horizontal"
          maxWidth="100%"
          ariaLabel="Horizontal preview strip"
        >
          <div style="display:flex;gap:0.75rem;width:max-content;padding-bottom:0.25rem">
            @for (item of horizontalItems; track item.id) {
              <div
                style="width:11rem;padding:0.875rem 1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
              >
                <div style="font-size:0.875rem;font-weight:600">{{ item.title }}</div>
                <div
                  style="margin-top:0.25rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
                >
                  {{ item.body }}
                </div>
              </div>
            }
          </div>
        </ui-scroll-panel>
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.875rem;font-weight:600">Both directions</div>
        <ui-scroll-panel
          orientation="both"
          maxHeight="16rem"
          maxWidth="100%"
          ariaLabel="Bidirectional canvas"
        >
          <div
            style="display:grid;grid-template-columns:repeat(6,10rem);gap:0.75rem;width:max-content;padding-right:0.25rem"
          >
            @for (item of canvasItems; track item.id) {
              <div
                style="height:7rem;padding:0.875rem 1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:0.875rem;background:linear-gradient(180deg,var(--color-neutral-background-rest),var(--color-neutral-background2-rest))"
              >
                <div style="font-size:0.875rem;font-weight:600">{{ item.title }}</div>
                <div
                  style="margin-top:0.25rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
                >
                  {{ item.body }}
                </div>
              </div>
            }
          </div>
        </ui-scroll-panel>
      </div>
    </div>
  `,
})
export class ScrollPanelOrientationDemoComponent {
  protected readonly horizontalItems = Array.from({ length: 8 }, (_, index) => ({
    id: index + 1,
    title: `Preview ${index + 1}`,
    body: 'Useful for media strips, cards, or dense option galleries.',
  }));

  protected readonly canvasItems = Array.from({ length: 12 }, (_, index) => ({
    id: index + 1,
    title: `Widget ${index + 1}`,
    body: 'Bidirectional scroll is useful only when the content truly needs two axes.',
  }));
}
```

## Scrollbar visibility behavior
```ts
import { Component } from '@angular/core';
import { ScrollPanelComponent } from 'ui';

@Component({
  selector: 'app-scroll-panel-scrollbar-behavior-demo',
  standalone: true,
  imports: [ScrollPanelComponent],
  template: `
    <div
      style="display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:1rem;width:100%;max-width:48rem"
    >
      @for (behavior of behaviors; track behavior) {
        <div
          style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
        >
          <div style="font-size:0.875rem;font-weight:600">{{ behavior }}</div>
          <ui-scroll-panel
            [scrollbarBehavior]="behavior"
            maxHeight="14rem"
            ariaLabel="Scrollbar behavior demo"
          >
            <div style="display:flex;flex-direction:column;gap:0.625rem">
              @for (item of items; track item.id) {
                <div
                  style="padding:0.75rem 0.875rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:0.75rem;background:var(--color-neutral-background-rest);font-size:0.8125rem"
                >
                  {{ item.label }}
                </div>
              }
            </div>
          </ui-scroll-panel>
        </div>
      }
    </div>
  `,
})
export class ScrollPanelScrollbarBehaviorDemoComponent {
  protected readonly behaviors = ['auto', 'always', 'never'] as const;
  protected readonly items = Array.from({ length: 10 }, (_, index) => ({
    id: index + 1,
    label: `Scrollable row ${index + 1}`,
  }));
}
```

## Programmatic scrolling
```ts
import { Component, signal, viewChild } from '@angular/core';
import { ButtonComponent, ScrollPanelComponent } from 'ui';

@Component({
  selector: 'app-scroll-panel-programmatic-demo',
  standalone: true,
  imports: [ButtonComponent, ScrollPanelComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:34rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" size="small" (click)="scrollToTop()"
          >Top</ui-button
        >
        <ui-button variant="secondary" appearance="outline" size="small" (click)="scrollToBottom()"
          >Bottom</ui-button
        >
        <ui-button variant="secondary" appearance="outline" size="small" (click)="scrollToRight()"
          >Right</ui-button
        >
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Last action: <strong>{{ lastAction() || 'none' }}</strong></span
        >
      </div>

      <div
        style="border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest);padding:0.5rem"
      >
        <ui-scroll-panel
          #panel
          orientation="both"
          maxHeight="16rem"
          ariaLabel="Programmatic scroll panel"
        >
          <div
            style="display:grid;grid-template-columns:repeat(5,12rem);gap:0.75rem;width:max-content"
          >
            @for (item of items; track item.id) {
              <div
                style="height:8rem;padding:0.875rem 1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
              >
                <div style="font-size:0.875rem;font-weight:600">{{ item.title }}</div>
                <div
                  style="margin-top:0.25rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
                >
                  {{ item.body }}
                </div>
              </div>
            }
          </div>
        </ui-scroll-panel>
      </div>
    </div>
  `,
})
export class ScrollPanelProgrammaticDemoComponent {
  protected readonly panel = viewChild<ScrollPanelComponent>('panel');
  protected readonly lastAction = signal('');

  protected readonly items = Array.from({ length: 10 }, (_, index) => ({
    id: index + 1,
    title: `Card ${index + 1}`,
    body: 'Programmatic scrolling is useful when the shell needs to jump to a region after an external action.',
  }));

  protected scrollToTop(): void {
    this.panel()?.scrollToTop();
    this.lastAction.set('Scrolled to top');
  }

  protected scrollToBottom(): void {
    this.panel()?.scrollToBottom();
    this.lastAction.set('Scrolled to bottom');
  }

  protected scrollToRight(): void {
    this.panel()?.scrollToRight();
    this.lastAction.set('Scrolled to right edge');
  }
}
```

## Scroll and scroll-end events
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, ScrollPanelComponent } from 'ui';

@Component({
  selector: 'app-scroll-panel-events-demo',
  standalone: true,
  imports: [ButtonComponent, ScrollPanelComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:30rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()"
          >Reset counters</ui-button
        >
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Top: <strong>{{ scrollTop() }}px</strong></span
        >
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Ended: <strong>{{ scrollEndCount() }}</strong></span
        >
      </div>

      <div
        style="border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest);padding:0.5rem"
      >
        <ui-scroll-panel
          maxHeight="18rem"
          ariaLabel="Scroll events demo"
          (scroll)="onScroll($event)"
          (scrollEnd)="onScrollEnd()"
        >
          <div style="display:flex;flex-direction:column;gap:0.75rem">
            @for (item of items; track item.id) {
              <div
                style="padding:0.875rem 1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
              >
                <div style="font-size:0.875rem;font-weight:600">{{ item.title }}</div>
                <div
                  style="margin-top:0.25rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
                >
                  {{ item.body }}
                </div>
              </div>
            }
          </div>
        </ui-scroll-panel>
      </div>
    </div>
  `,
})
export class ScrollPanelEventsDemoComponent {
  protected readonly scrollTop = signal(0);
  protected readonly scrollEndCount = signal(0);

  protected readonly items = Array.from({ length: 16 }, (_, index) => ({
    id: index + 1,
    title: `Event row ${index + 1}`,
    body: 'Listening to scroll and scrollEnd is useful for analytics, sticky shell logic, or lazy UI reactions.',
  }));

  protected onScroll(event: Event): void {
    const target = event.target as HTMLElement;
    this.scrollTop.set(Math.round(target.scrollTop));
  }

  protected onScrollEnd(): void {
    this.scrollEndCount.update(count => count + 1);
  }

  protected reset(): void {
    this.scrollTop.set(0);
    this.scrollEndCount.set(0);
  }
}
```

## Inbox or side-panel composition
```ts
import { Component } from '@angular/core';
import { MessageBarComponent, ScrollPanelComponent, SearchComponent } from 'ui';

@Component({
  selector: 'app-scroll-panel-inbox-layout-demo',
  standalone: true,
  imports: [MessageBarComponent, ScrollPanelComponent, SearchComponent],
  template: `
    <div
      style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:34rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <div>
        <div style="font-size:0.9375rem;font-weight:600">Inbox layout</div>
        <div
          style="margin-top:0.25rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
        >
          A scroll panel is often just the scroll shell inside a denser composition with search and
          status.
        </div>
      </div>

      <ui-message-bar
        variant="info"
        title="7 unread updates"
        message="Keep the shell compact and let only the list region scroll."
        appearance="subtle"
      />

      <ui-search placeholder="Search messages" style="width:100%" />

      <div
        style="border:1px solid var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest);padding:0.5rem"
      >
        <ui-scroll-panel maxHeight="20rem" ariaLabel="Inbox messages panel">
          <div style="display:flex;flex-direction:column;gap:0.625rem">
            @for (item of items; track item.id) {
              <div
                style="padding:0.875rem 1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
              >
                <div style="font-size:0.875rem;font-weight:600">{{ item.title }}</div>
                <div
                  style="margin-top:0.1875rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
                >
                  {{ item.body }}
                </div>
              </div>
            }
          </div>
        </ui-scroll-panel>
      </div>
    </div>
  `,
})
export class ScrollPanelInboxLayoutDemoComponent {
  protected readonly items = Array.from({ length: 12 }, (_, index) => ({
    id: index + 1,
    title: index % 2 === 0 ? `Review request ${index + 1}` : `Mention from design ${index + 1}`,
    body: 'This kind of panel usually needs one clearly bounded scroll area inside a bigger shell.',
  }));
}
```

## Accessibility

### Scrollable region semantics
`ui-scroll-panel` renders a region with an accessible label. Supply a more specific `ariaLabel` when the generic scrollable-content wording is not enough for the surrounding UI.

### Keyboard behavior
Keyboard behavior mainly follows native scroll container expectations.

| Key | Action |
| --- | --- |
| Tab / Shift+Tab | Moves focus into or past interactive content inside the panel. |
| Arrow keys / Page keys | Follow browser scrolling behavior when the region or an inner control has focus. |
| Home / End | Can move to the start or end of scrollable content depending on browser and focus context. |

### Bounded scroll areas
Keep only the intended region scrollable. Nested scroll areas become hard to operate if the panel is not clearly bounded or if its label does not explain what the user is scrolling.
