{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "lina-base",
  "type": "registry:ui",
  "title": "Base UI Adaptive Scroll Area",
  "description": "A responsive scroll area that feel native on touch devices, offereing custom styling and enchanced interactions where it matter most.",
  "dependencies": [
    "@base-ui-components/react"
  ],
  "devDependencies": [
    "tw-animate-css"
  ],
  "files": [
    {
      "path": "registry/base-ui/scroll-area.tsx",
      "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\n\r\nimport { ScrollArea as ScrollAreaPrimitive } from \"@base-ui-components/react/scroll-area\";\r\n\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nimport { useTouchPrimary } from \"@/hooks/use-has-primary-touch\";\r\n\r\ntype Mask = {\r\n  top: boolean;\r\n  bottom: boolean;\r\n  left: boolean;\r\n  right: boolean;\r\n};\r\n\r\nexport type ScrollAreaContextProps = {\r\n  isTouch: boolean;\r\n  type: \"auto\" | \"always\" | \"scroll\" | \"hover\";\r\n};\r\n\r\nconst ScrollAreaContext = React.createContext<ScrollAreaContextProps>({\r\n  isTouch: false,\r\n  type: \"hover\",\r\n});\r\n\r\nconst ScrollArea = React.forwardRef<\r\n  React.ComponentRef<typeof ScrollAreaPrimitive.Root>,\r\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {\r\n    type?: \"auto\" | \"always\" | \"scroll\" | \"hover\";\r\n    viewportClassName?: string;\r\n    /**\r\n     * `maskHeight` is the height of the mask in pixels.\r\n     * pass `0` to disable the mask\r\n     * @default 30\r\n     */\r\n    maskHeight?: number;\r\n    maskClassName?: string;\r\n  }\r\n>(({ className, children, type = \"hover\", maskHeight = 30, maskClassName, viewportClassName, ...props }, ref) => {\r\n  const [showMask, setShowMask] = React.useState<Mask>({\r\n    top: false,\r\n    bottom: false,\r\n    left: false,\r\n    right: false,\r\n  });\r\n\r\n  const viewportRef = React.useRef<HTMLDivElement>(null);\r\n  const isTouch = useTouchPrimary();\r\n\r\n  const checkScrollability = React.useCallback(() => {\r\n    const element = viewportRef.current;\r\n    if (!element) return;\r\n\r\n    const { scrollTop, scrollLeft, scrollWidth, clientWidth, scrollHeight, clientHeight } = element;\r\n    setShowMask((prev) => ({\r\n      ...prev,\r\n      top: scrollTop > 0,\r\n      bottom: scrollTop + clientHeight < scrollHeight - 1,\r\n      left: scrollLeft > 0,\r\n      right: scrollLeft + clientWidth < scrollWidth - 1,\r\n    }));\r\n  }, []);\r\n\r\n  React.useEffect(() => {\r\n    if (typeof window === \"undefined\") return;\r\n\r\n    const element = viewportRef.current;\r\n    if (!element) return;\r\n\r\n    const controller = new AbortController();\r\n    const { signal } = controller;\r\n\r\n    const resizeObserver = new ResizeObserver(checkScrollability);\r\n    resizeObserver.observe(element);\r\n\r\n    element.addEventListener(\"scroll\", checkScrollability, { signal });\r\n    window.addEventListener(\"resize\", checkScrollability, { signal });\r\n\r\n    // Run an initial check whenever dependencies change (including pointer mode)\r\n    checkScrollability();\r\n\r\n    return () => {\r\n      controller.abort();\r\n      resizeObserver.disconnect();\r\n    };\r\n  }, [checkScrollability, isTouch]);\r\n\r\n  return (\r\n    <ScrollAreaContext.Provider value={{ isTouch, type }}>\r\n      {isTouch ? (\r\n        <div\r\n          ref={ref}\r\n          {...props}\r\n          role=\"group\"\r\n          data-slot=\"scroll-area\"\r\n          aria-roledescription=\"scroll area\"\r\n          className={cn(\"relative overflow-hidden\", className)}\r\n        >\r\n          <div ref={viewportRef} className={cn(\"size-full overflow-auto\", viewportClassName)} tabIndex={0}>\r\n            {children}\r\n          </div>\r\n          {maskHeight > 0 && <ScrollMask showMask={showMask} className={maskClassName} maskHeight={maskHeight} />}\r\n        </div>\r\n      ) : (\r\n        <ScrollAreaPrimitive.Root\r\n          ref={ref}\r\n          data-slot=\"scroll-area\"\r\n          className={cn(\"relative overflow-hidden\", viewportClassName, className)}\r\n          {...props}\r\n        >\r\n          <ScrollAreaPrimitive.Viewport\r\n            ref={viewportRef}\r\n            data-slot=\"scroll-area-viewport\"\r\n            className={cn(\"focus-ring size-full rounded-[inherit]\", viewportClassName)}\r\n          >\r\n            {children}\r\n          </ScrollAreaPrimitive.Viewport>\r\n          <ScrollBar />\r\n          <ScrollAreaPrimitive.Corner />\r\n          {maskHeight > 0 && <ScrollMask showMask={showMask} className={maskClassName} maskHeight={maskHeight} />}\r\n        </ScrollAreaPrimitive.Root>\r\n      )}\r\n    </ScrollAreaContext.Provider>\r\n  );\r\n});\r\n\r\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\r\n\r\nconst ScrollBar = React.forwardRef<\r\n  React.ComponentRef<typeof ScrollAreaPrimitive.Scrollbar>,\r\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Scrollbar>\r\n>(({ className, orientation = \"vertical\", ...props }, ref) => {\r\n  const { isTouch, type } = React.useContext(ScrollAreaContext);\r\n\r\n  if (isTouch) return null;\r\n\r\n  return (\r\n    <ScrollAreaPrimitive.Scrollbar\r\n      ref={ref}\r\n      orientation={orientation}\r\n      data-slot=\"scroll-area-scrollbar\"\r\n      className={cn(\r\n        \"hover:bg-muted dark:hover:bg-muted/50 flex touch-none p-px transition-[colors,opacity] duration-150 ease-out select-none\",\r\n        orientation === \"vertical\" && \"h-full w-2.5 border-l border-l-transparent\",\r\n        orientation === \"horizontal\" && \"h-2.5 flex-col border-t border-t-transparent px-1 pr-1.25\",\r\n        type === \"hover\" && \"opacity-0 data-[hovering]:opacity-100\",\r\n        type === \"scroll\" && \"opacity-0 data-[scrolling]:opacity-100\",\r\n        className\r\n      )}\r\n      {...props}\r\n    >\r\n      <ScrollAreaPrimitive.Thumb\r\n        data-slot=\"scroll-area-thumb\"\r\n        className={cn(\r\n          \"bg-border relative flex-1 rounded-full transition-[scale]\",\r\n          orientation === \"vertical\" && \"my-1 active:scale-y-95\",\r\n          orientation === \"horizontal\" && \"active:scale-x-98\"\r\n        )}\r\n      />\r\n    </ScrollAreaPrimitive.Scrollbar>\r\n  );\r\n});\r\n\r\nScrollBar.displayName = ScrollAreaPrimitive.Scrollbar.displayName;\r\n\r\nconst ScrollMask = ({\r\n  showMask,\r\n  maskHeight,\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<\"div\"> & {\r\n  showMask: Mask;\r\n  maskHeight: number;\r\n}) => {\r\n  return (\r\n    <>\r\n      <div\r\n        {...props}\r\n        aria-hidden=\"true\"\r\n        style={\r\n          {\r\n            \"--top-fade-height\": showMask.top ? `${maskHeight}px` : \"0px\",\r\n            \"--bottom-fade-height\": showMask.bottom ? `${maskHeight}px` : \"0px\",\r\n          } as React.CSSProperties\r\n        }\r\n        className={cn(\r\n          \"pointer-events-none absolute inset-0 z-10\",\r\n          \"before:absolute before:inset-x-0 before:top-0 before:transition-[height,opacity] before:duration-300 before:content-['']\",\r\n          \"after:absolute after:inset-x-0 after:bottom-0 after:transition-[height,opacity] after:duration-300 after:content-['']\",\r\n          \"before:h-(--top-fade-height) after:h-(--bottom-fade-height)\",\r\n          showMask.top ? \"before:opacity-100\" : \"before:opacity-0\",\r\n          showMask.bottom ? \"after:opacity-100\" : \"after:opacity-0\",\r\n          \"before:from-background before:bg-gradient-to-b before:to-transparent\",\r\n          \"after:from-background after:bg-gradient-to-t after:to-transparent\",\r\n          className\r\n        )}\r\n      />\r\n      <div\r\n        {...props}\r\n        aria-hidden=\"true\"\r\n        style={\r\n          {\r\n            \"--left-fade-width\": showMask.left ? `${maskHeight}px` : \"0px\",\r\n            \"--right-fade-width\": showMask.right ? `${maskHeight}px` : \"0px\",\r\n          } as React.CSSProperties\r\n        }\r\n        className={cn(\r\n          \"pointer-events-none absolute inset-0 z-10\",\r\n          \"before:absolute before:inset-y-0 before:left-0 before:transition-[width,opacity] before:duration-300 before:content-['']\",\r\n          \"after:absolute after:inset-y-0 after:right-0 after:transition-[width,opacity] after:duration-300 after:content-['']\",\r\n          \"before:w-(--left-fade-width) after:w-(--right-fade-width)\",\r\n          showMask.left ? \"before:opacity-100\" : \"before:opacity-0\",\r\n          showMask.right ? \"after:opacity-100\" : \"after:opacity-0\",\r\n          \"before:from-background before:bg-gradient-to-r before:to-transparent\",\r\n          \"after:from-background after:bg-gradient-to-l after:to-transparent\",\r\n          className\r\n        )}\r\n      />\r\n    </>\r\n  );\r\n};\r\n\r\nexport { ScrollArea, ScrollBar };\r\n",
      "type": "registry:ui"
    },
    {
      "path": "hooks/use-has-primary-touch.tsx",
      "content": "import { useEffect, useState } from \"react\";\r\n\r\nexport function useTouchPrimary() {\r\n  const [isTouchPrimary, setIsTouchPrimary] = useState(false);\r\n\r\n  useEffect(() => {\r\n    if (typeof window === \"undefined\") return;\r\n\r\n    const controller = new AbortController();\r\n    const { signal } = controller;\r\n\r\n    const handleTouch = () => {\r\n      const hasTouch = \"ontouchstart\" in window || navigator.maxTouchPoints > 0;\r\n      const prefersTouch = window.matchMedia(\"(pointer: coarse)\").matches;\r\n      setIsTouchPrimary(hasTouch && prefersTouch);\r\n    };\r\n\r\n    const mq = window.matchMedia(\"(pointer: coarse)\");\r\n    mq.addEventListener(\"change\", handleTouch, { signal });\r\n    window.addEventListener(\"pointerdown\", handleTouch, { signal });\r\n\r\n    handleTouch();\r\n\r\n    return () => controller.abort();\r\n  }, []);\r\n\r\n  return isTouchPrimary;\r\n}\r\n",
      "type": "registry:ui"
    }
  ]
}