diff --git a/CHANGELOG.md b/CHANGELOG.md
index 16b0e55..933a97a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- TimePicker component by [@amit-y](https://github.com/amit-y)
+
## [1.17.1] - 2023-12-18
- add option to enable title's tooltip + change up/down trend arrow colors to grey by [@shahramk](https://github.com/shahramk)
diff --git a/README.md b/README.md
index 0524cbf..6cf82ac 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,9 @@ Component that allows a user to filter options.
### [DatePicker](src/components/date-picker)
Component that displays a calendar to select a date
+### [TimePicker](src/components/time-picker)
+Component to select time values
+
## Utilities
### [timeRangeToNrql](src/utils/time-range-to-nrql/)
diff --git a/src/components/index.js b/src/components/index.js
index 312e90f..a1fc90a 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -9,3 +9,4 @@ export { default as StatusIconsLayout } from './status-icons-layout';
export { default as ProgressBar } from './progress-bar';
export { default as FilterBar } from './filter-bar';
export { default as DatePicker } from './date-picker';
+export { default as TimePicker } from './time-picker';
diff --git a/src/components/time-picker/README.md b/src/components/time-picker/README.md
new file mode 100644
index 0000000..aaaa2b1
--- /dev/null
+++ b/src/components/time-picker/README.md
@@ -0,0 +1,28 @@
+# TimePicker
+
+The `TimePicker` component is used to select a time value from a list of valid times.
+
+## Usage
+
+To use the component, simply import and use:
+
+```jsx
+import React, { useState } from 'react';
+import { TimePicker } from '@newrelic/nr-labs-components';
+
+function MyComponent() {
+ const [selectedTime, setSelectedTime] = useState(new Date());
+
+ return (
+
+
+
+ );
+}
+```
+### Props
+
+- time (date): The default time
+- onChange (function): A function that is called when the user selects a time
+- validFrom (date): only allow times starting from the provided value
+- validTill (date): only allow times ending till the provided value
diff --git a/src/components/time-picker/index.js b/src/components/time-picker/index.js
new file mode 100644
index 0000000..0777ff6
--- /dev/null
+++ b/src/components/time-picker/index.js
@@ -0,0 +1,162 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+
+import {
+ Popover,
+ PopoverTrigger,
+ PopoverBody,
+ TextField,
+ List,
+ ListItem,
+ Button,
+} from 'nr1';
+
+import {
+ getHourMinuteFromTimeString,
+ isValidTime,
+ normalizedDateTime,
+ isHourMinuteNumbers,
+} from './utils';
+
+import styles from './styles.scss';
+
+const TIME_FORMATTER = new Intl.DateTimeFormat('default', {
+ hour12: true,
+ hour: 'numeric',
+ minute: 'numeric',
+});
+
+const TIMES_LIST = Array.from({ length: 48 }).map((_, i) => ({
+ key: `time${i}`,
+ value:
+ `${Math.floor(i / 2) % 12 || 12}`.padStart(2, '0') +
+ `:${i % 2 ? '30' : '00'} ${i > 23 ? 'pm' : 'am'}`,
+}));
+
+const TimePicker = ({ time, validFrom, validTill, onChange }) => {
+ const [opened, setOpened] = useState(false);
+ const [filter, setFilter] = useState('');
+ const [times, setTimes] = useState([]);
+
+ useEffect(() => {
+ if (!filter) {
+ setTimes(
+ TIMES_LIST.filter(({ value }) =>
+ isValidTime(value, validFrom, validTill, time)
+ )
+ );
+ return;
+ }
+ const re = /^(1[0-2]|0?[1-9]):?([0-5]?[0-9])? ?([AaPp][Mm]?)?/;
+ const [, hr, mi, me] = filter.match(re) || [];
+
+ if (hr) {
+ let reStr = hr;
+ if (reStr.length === 1 && Number(hr) < 3) reStr += '[0-9]?';
+ reStr += mi ? `:${mi}` : ':[0-9][0-9]';
+ if (reStr && me && /^[Aa]/.test(me)) reStr += ' am';
+ if (reStr && me && /^[Pp]/.test(me)) reStr += ' pm';
+
+ const re2 = new RegExp(reStr);
+ const matches = TIMES_LIST.filter(
+ ({ value }) =>
+ value.match(re2) && isValidTime(value, validFrom, validTill, time)
+ );
+ if (!matches.length && hr && mi && mi.length === 2) {
+ if (me && /^[AaPp]/.test(me)) {
+ const value = `${hr}:${mi} ${/^[Aa]/.test(me) ? 'am' : 'pm'}`;
+ if (isValidTime(value, validFrom, validTill, time))
+ matches.push({
+ key: 'time48',
+ value,
+ });
+ } else {
+ ['am', 'pm'].forEach((m) => {
+ const value = `${hr}:${mi} ${m}`;
+ if (isValidTime(value, validFrom, validTill, time))
+ matches.push({
+ key: `time48${m}`,
+ value,
+ });
+ });
+ }
+ }
+ setTimes(matches);
+ }
+ }, [filter]);
+
+ const clickHandler = useCallback((e, t) => {
+ e.stopPropagation();
+ const { hr, mi } = getHourMinuteFromTimeString(t);
+ if (!isHourMinuteNumbers(hr, mi)) return;
+ if (onChange) onChange(normalizedDateTime(time, hr, mi));
+ setOpened(false);
+ setFilter('');
+ }, []);
+
+ const changeHandler = useCallback((_, o) => {
+ if (!o) setFilter('');
+ setOpened(o);
+ });
+
+ const filterChangeHandler = useCallback(
+ ({ target: { value = '' } } = {}) => setFilter(value),
+ []
+ );
+
+ const keyDownHandler = useCallback((e) => {
+ const re = /[0-9APMapm: ]+/g;
+ if (!re.test(e.key)) e.preventDefault();
+ }, []);
+
+ return (
+
+
+
+
+
+