{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/scroll-view",
  "title": "Scroll View",
  "description": "Scrollable container with scrollbar",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/scroll-view.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React, { useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useInteraction } from \"@/hooks/use-interaction\";\nimport type { InteractionProps } from \"@/hooks/use-interaction\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\n\nexport interface ScrollViewProps extends InteractionProps {\n  height: number;\n  children: ReactNode;\n  contentHeight?: number;\n  showScrollbar?: boolean;\n  scrollbarColor?: string;\n  thumbColor?: string;\n  trackChar?: string;\n  thumbChar?: string;\n  \"aria-label\"?: string;\n}\n\nexport const ScrollView = ({\n  height,\n  children,\n  contentHeight = 0,\n  showScrollbar = true,\n  scrollbarColor,\n  thumbColor,\n  trackChar,\n  thumbChar,\n  id,\n  autoFocus,\n  isActive,\n  disabled,\n  \"aria-label\": ariaLabel = \"Scrollable content\",\n}: ScrollViewProps) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const resolvedTrackChar = trackChar ?? (unicode ? \"│\" : \"|\");\n  const resolvedThumbChar = thumbChar ?? (unicode ? \"█\" : \"#\");\n  const [scrollTop, setScrollTop] = useState(0);\n\n  const resolvedTrackColor = scrollbarColor ?? theme.colors.mutedForeground;\n  const resolvedThumbColor = thumbColor ?? theme.colors.primary;\n\n  const maxScroll = Math.max(0, contentHeight - height);\n\n  const clampedScroll = Math.min(scrollTop, maxScroll);\n\n  const { isFocused } = useInteraction(\n    (_input, key) => {\n      if (key.upArrow) {\n        setScrollTop((s) => Math.max(0, s - 1));\n      } else if (key.downArrow) {\n        setScrollTop((s) => Math.min(maxScroll > 0 ? maxScroll : s + 1, s + 1));\n      } else if (key.pageUp) {\n        setScrollTop((s) => Math.max(0, s - height));\n      } else if (key.pageDown) {\n        setScrollTop((s) =>\n          maxScroll > 0 ? Math.min(maxScroll, s + height) : s + height\n        );\n      } else if (key.home) {\n        setScrollTop(0);\n      } else if (key.end && maxScroll > 0) {\n        setScrollTop(maxScroll);\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  const thumbSize = useMemo(() => {\n    if (contentHeight <= height) {\n      return height;\n    }\n    return Math.max(1, Math.round((height / contentHeight) * height));\n  }, [contentHeight, height]);\n\n  const thumbPosition = useMemo(() => {\n    if (contentHeight <= height || maxScroll === 0) {\n      return 0;\n    }\n    return Math.round((clampedScroll / maxScroll) * (height - thumbSize));\n  }, [clampedScroll, maxScroll, height, thumbSize, contentHeight]);\n\n  const scrollbar = useMemo(\n    () =>\n      Array.from({ length: height }, (_, i) => {\n        const isThumb = i >= thumbPosition && i < thumbPosition + thumbSize;\n        return {\n          char: isThumb ? resolvedThumbChar : resolvedTrackChar,\n          isThumb,\n        };\n      }),\n    [height, resolvedThumbChar, resolvedTrackChar, thumbPosition, thumbSize]\n  );\n\n  return (\n    <Box\n      flexDirection=\"row\"\n      height={height}\n      overflow=\"hidden\"\n      aria-role=\"list\"\n      aria-state={{ disabled: disabled || undefined }}\n    >\n      <Text\n        aria-label={`${ariaLabel}. Line ${clampedScroll + 1} of ${Math.max(height, contentHeight)}.${isFocused ? \" Focused.\" : \"\"}`}\n      >\n        {\"\"}\n      </Text>\n      <Box\n        flexGrow={1}\n        flexDirection=\"column\"\n        marginTop={-clampedScroll as number}\n      >\n        {children}\n      </Box>\n      {showScrollbar && (\n        <Box aria-hidden width={1} flexDirection=\"column\">\n          {scrollbar.map((seg, i) => (\n            <Text\n              key={i}\n              color={seg.isThumb ? resolvedThumbColor : resolvedTrackColor}\n            >\n              {seg.char}\n            </Text>\n          ))}\n        </Box>\n      )}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "layout"
  ],
  "type": "registry:ui"
}